Repro:
$ abbr -h | less
fish: Job 1, 'abbr --help | less' has stopped
Bisects to 5ece9bec6c (__fish_print_help: use man as-is, 2025-08-29).
Root cause is similar to
https://github.com/fish-shell/fish-shell/issues/8699#issuecomment-1030762748
except it's specific to how __fish_print_help is invoked, since
"__fish_print_help abbr | less" works fine.
Fix this by letting __fish_print_help access the terminal.
Fixes#12612
Solved by Claude in a single prompt:
---
▐▛███▜▌ Claude Code v2.1.92
▝▜█████▛▘ Sonnet 4.6
▘▘ ▝▝ ~/git/fish-shell
❯ "abbr -h | less" gets a signal, resulting in the message "fish: Job 1, 'abbr -h | less' has stopped"
whereas "__fish_print_help abbr | less" works just fine, even though "abbr -h" should run exactly that.
Why this difference?
[...]
● Now I can give you a clear answer.
---
The difference comes down to job group inheritance when man is
spawned.
The deferred execution order
In abbr -h | less, fish uses the "deferred process" optimization:
since abbr is a non-external process piping into an external one,
fish launches less first,
transfers the terminal to less's process group, then runs the
deferred abbr builtin. By the time abbr's help function spawns man,
the terminal already
belongs to less's pgroup.
Same thing happens with __fish_print_help abbr | less — the outer
less is launched first and gets the terminal.
Where the paths diverge
Function case (__fish_print_help abbr | less):
get_performer_for_function (exec.rs:1050) captures job.group.clone()
and passes it to eval_node. So when command man runs inside the
function, it's given the outer pipeline's job group — the same
one that owns the terminal. man's inner less pager joins that group
and can read from the terminal freely. ✓
Builtin case (abbr -h | less): builtin_print_help calls
parser.eval(&cmd, streams.io_chain) (shared.rs:652). parser.eval
is defined as:
pub fn eval(&self, cmd: &wstr, io: &IoChain) -> EvalRes {
self.eval_with(cmd, io, None, BlockType::top, false)
// ^^^^ job_group = None
}
job_group = None means man creates a brand-new job group with no
terminal access. When man spawns its less pager, the pager tries
to read keyboard input from
the terminal, but the terminal belongs to the outer less's process
group. The pager receives SIGTTIN → stops → the whole job is
reported as stopped.
The fix
builtin_print_help should pass streams.job_group to the eval call
(which is already populated at exec.rs:1192). Changing line 652 from:
let res = parser.eval(&cmd, streams.io_chain);
to:
let res = parser.eval_with(&cmd, streams.io_chain, streams.job_group.as_ref(), BlockType::top, false);
would give man the outer job's job group, letting its pager access
the terminal just like the function case.
✻ Sautéed for 13m 0s
@@ -54,7 +54,7 @@ It also provides a large number of program specific scripted completions. Most o
You can also write your own completions or install some you got from someone else. For that, see :doc:`Writing your own completions <completions>`.
Completion scripts are loaded on demand, like :ref:`functions are <syntax-function-autoloading>`. The difference is the ``$fish_complete_path``:ref:`list <variables-lists>` is used instead of ``$fish_function_path``. Typically you can drop new completions in ~/.config/fish/completions/name-of-command.fish and fish will find them automatically.
Completion scripts are loaded on demand, like :ref:`functions are <syntax-function-autoloading>`. The difference is the ``$fish_complete_path``:ref:`list <variables-lists>` is used instead of ``$fish_function_path``. Typically you can drop new completions in ``~/.config/fish/completions/<name-of-command>.fish`` and fish will find them automatically.
isolated-tmux send-keys "abbr --help | sed 1q | less" Enter
tmux-sleep
isolated-tmux capture-pane -p
# CHECK: (END)
# CHECK: prompt 0>abbr--help|sed 1q |less
# CHECK: Documentation for abbr
isolated-tmux send-keys q
# Expand abbreviations on space.
isolated-tmux send-keys abbr-test Space arg1 Enter
tmux-sleep
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.