On Windows, "chmod 000 .; cd ." doesn't fail, so disable this
test case on Windows. On the Windows CI runner, "uname" prints
"MINGW64_NT-10.0-26100" so we can't use "__fish_is_cygwin".
Simplify these and move the sanitization closer to the point of output.
Stop worrying about control characters beyond what PCRE2 reports in
"[[:cntrl:]]" for clarity.
Strip control characters from VCS branch and state strings before writing them in prompts.
Also route the informative and minimalist sample prompts through prompt_pwd instead of printing PWD directly.
Doing
firefox --pro TAB TAB TAB
results in
firefox --profile --ProfileManager
git-bisect points to 3546ffa3ef (reader
handle_completions(): remove dead filtering code, 2026-01-02)
but that regression has already been fixed by 85e76ba356
(Fix option substr completions not being filtered out, 2026-04-16).
However in between those two commits, the above case has also been
broken by 2f6b1eaaf9 (reader handle_completions(): don't consider
odd replacing completions for common prefix, 2026-01-02)
The first TAB inserts "--profile ", including the trailing space.
However it also shows the completion pager, which means that subsequent
TABs will insert after the space.
The trailing space does not make sense unless we navigate the pager.
Remove it in all cases, to fix this smartcase scenario.
Alternatively, we could start navigating the pager in this case
(and keep the trailing space), but that's probably too inconsistent.
git status --porcelain -z outputs the rename source filename as a
separate NUL-delimited entry after the status line. When the source
filename starts with [ACDMRTU], the informative prompt miscounts it
as an additional staged change.
Part of #12754
When we push a new reader for builtin read, we use the default
CursorSelectionMode::Exclusive, which is wrong in Vi mode.
Add a haphazard fix for that.
This is very ugly, we should improve this.
Closes#12724
This removes the need to double-escape the values on the command line
(once for the command line parser, another for the option handling)
This also brings it in line with the implicit case (`complete cmd ...`)
Fixes#12712Closes#12718
During our one-time migration away from universal variables,
we create ~/.config/fish/conf.d/fish_frozen_theme.fish
if we think that the current theme is different from the default.
The default uvar-backed theme had changed over time, but existing
installations would not be upgraded. Because of this, we have
a heuristic that assumes that values coinciding with historical
default values also stem from a default. Some historical values are
missing. Add them.
There are more left, see 03b23dd1b6 (Update default colors,
2022-01-27).
Fixes#12725
If a user passes "-i" when running a script, they ought to expect
weird behavior i.e. fish might run the user's interactive-only
configuration which might print things to TTY etc. But at least
for our part of the configuration, we can avoid depending on the
user-settable interactive bit.
__fish_config_interactive is already only called when we paint the
first prompt, either for a prompt (which implies we're an interactive
shell) or for builtin read (which does not imply anything about the
interactivity of the shell).
Only print greetings when not in interactive read. Notably, "status
is-interactive-read" is not overridable by the user.
This helps us get rid of more "status is-interactive" switches.
This command
cd $(mktemp -d)
mkdir a "a b"
complete -C": "
prints
a b/
a/
which is wrong ordering.
Usually the trailing slash should not be compared.
Fix this by always sorting slashes first. Not sure if this is correct
for middle slashes but I couldn't find a case where it matters.
Closes#12695
Rewrite the PO file handling logic in Rust and make it available via an
xtask. Replaces the
`build_tools/{update_translations,fish_xgettext}.fish` scripts.
Main benefits:
- Better ergonomics
- Better error handling
- Eliminates the need for a fish executable for updating PO files,
which is particularly useful in CI
- Improved performance, mainly due to concurrent threads working on the
PO files in parallel
The behavior is mostly unchanged, with the minor exception that section
headers for empty sections are now omitted in PO files.
The interface for invoking the tooling is quite different. Instead of
working with flags, `cargo xtask gettext` has 3 subcommands:
- `update` modifies the PO files to match the current sources
- `check` is like update, but instead of modifying the PO files, it
shows diffs between the current version of the PO files and what they
would look like after updating. When there is a difference, the xtask
exits non-zero, making it useful for checks to detect outdated PO
files.
- `new` creates a new PO file for the given language.
Both the `update` and `check` command take any number of file paths to
specify the PO files to consider. If none are specified, all files in
`localization/po/` are considered.
Extracting gettext messages from Rust still requires compiling with the
`gettext-extract` feature active. In situations where compilation is
needed for other purposes as well, it can make sense to only build once
and then tell the gettext xtask about the directory into which the
messages have been extracted. This can be done via the
`--rust-extraction-dir` flag. If we stop having gettext messages in
Rust, this logic can be removed.
Closes#12676
Commit 3534c07584 (Adopt the new AST in parse_execution, 2020-07-03)
added to parse_execution_context_t::run_job_conjunction an early
return when any job in a job conjunction fails to launch. This causes
"nosuchcommand || echo hello" to not execute the continuation.
Fix this by restoring the previous behavior.
Fixes#12654
We define colors in noninteractive shells for historical reasons
(because colors used to be universal variables).
The other potential reason is to get regular syntax highlighting for
commands like:
fish -c 'read --shell'
but if anyone actually uses that they can probably load a theme
explicitly.
Stop defining colors in noninteractive shells. It's usually not
a good idea to make them behave differently from interactive ones,
but color seems only relevant for interactive shells?
Let's see if anyone complains.. we may end up reverting this if people
want to use noninteractive fish to query colors.. but I'm not sure
why that would be necessary.
Closes#12673
"commandline -f repaint" might be triggered for various reasons;
since this sets "last_cmd", it will reset some UI states, notably
pager selection:
1. press tab
2. trigger repaint
3. press tab
The repaint prevents us from selecting the first candidate.
Work around this by ignoring repaint events for the last_cmd logic.
Fixes#12683
As seen in
https://github.com/fish-shell/fish-shell/actions/runs/24944417077/job/73043241890?pr=12171
Failure:
The CHECK on line 12 wants:
prompt 1> source -
which failed to match line stdout:3:
source -
Context:
prompt 0> source
source: missing filename argument or input redirection
source - <= no check matches this, previous check on line 11
prompt 1> source -
prompt 1>
Installing a program like sway to /usr/local installs fish
completions to /usr/local/share/fish/vendor_completions.d/sway.fish.
When $XDG_DATA_DIRS is empty, these will typically not
be picked up.
(Since "__extra_completionsdir" is usually
"/usr/share/fish/vendor_completions.d/", this issue typically only
affects "/usr/share", not "/usr".)
Fix this by using the correct fallback value for XDG_DATA_DIRS.
Fixes#11349Closes#12656
Most notably:
- Unlike MSYS, Cygwin seems to always properly handle symlinks (at least
in common scenarios)
- With ACL, "x" permission also requires "r" do to anything, be it files
or directories
Closes#12642
Intermittent test failure suggests that kill(3p) returns before the
signal is delivered. Fix the failure by waiting until the signal
has been delivered before continuing the test.
Fixes#12635
Commit 3546ffa3ef (reader handle_completions(): remove dead filtering code,
2026-01-02) gives a proof of correctness that still makes sense;
The first lemma ("if will_replace_token") is trivially true, so no need to
assert it.
The second lemma ("if !will_replace_token") is violated in some edge cases:
we claim that given a token "-c", the option completion "--clip" is an exact match,
which is not true, it's a substring match.
Fix that, asserting the claim.
If we get to this code path, we'll only get completions for user
names, so technically the full StringFuzzyMatch with its ranking of
samecase/smartcase/icase (only showing the best) might be overkill,
but it seems like a good idea to treat this the same way as other
completions.
The occasion for this commit is to correct a wrong
StringFuzzyMatch::exact_match() in the icase branch; which will be
important for a following commit. Add a test for that.
If a directory has a control sequence in it, then prompt_pwd (used in
the default prompt) would emit it to the console, which could cause
the terminal to interpret the escape sequence.
Strip control sequences from within prompt_pwd, in the same way as
we do in __fish_paste.fish, to sanitize it.
Closes#12629
The main changes are:
- disabling some checks related to POSIX file permissions when a filesystem is
mounted with "noacl" (default on MSYS2)
- disabling some checks related to symlinks when using fake ones (file copy)
Windows with acl hasn't been tested because 1) Cygwin itself does not have any
Rust package yet to compile fish, and 2) MSYS2 defaults to `noacl`
Part of #12171
- Use the different strings for different checks to more easily narrow down
where a failure happens
- Move CHECK comments outside a `if...else...end` to avoid giving the impression
that the check only runs in the `if` case.
Part of #12171
Previously, SIGTERM immediately re-raised with SIG_DFL, killing
fish without saving history. SIGHUP deferred via a flag but never
re-raised, so the parent saw a normal exit instead of signal death.
Unify both signals: the handler stores the signal number in a single
AtomicI32, the reader loop exits normally, throwing_main() saves
history and re-raises with SIG_DFL so the parent sees WIFSIGNALED.
Fixes#10300Closes#12615
To homogenize error reporting format, use a new Error struct. Currently this
is used for builtins and ensuring a common cmd/subcmd prefix.
Part of #12556
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
With a few exceptions, only one test is added for a given message, even
when there are multiple ways to trigger the same message (e.g. different
invalid option combinations, or triggered in shared functions such as
`builtin_unknown_option`)
Includes a few very minor fixes, such as missing a newline, or using the
wrong var name.
Closes#12603
- fix wrong pattern used in `string replace` error message
- replace unreachable error with `unreachable!` in `string`
- fix cmd being used in place of subcmd
Part of #12603
Previously executing `set -a` or `set -p` would just list all the
variables, which does not make sense since the user specifically ask
for an action (append/prepend).
Update the help page synopsis
Part of #12603
- Remove unreachable error message in `handle_env_return()`
While we could have put an empty block in `handle_env_return()` and
removed the condition on `NotFound` in `erase()`, we prefered to use
`unreachable!` in case `handle_env_return()` gets called in new scenarios
in the future
- Make reachable the error message when asking to show a slice
Part of #12603