fish 3.0 changes command composition and variable scope
fish 3.0 accepts familiar logical operators while retaining its own language. Changes to scope, expansion, and builtin arithmetic give existing scripts more substantial migration work.
A shell is both an interactive interface and a programming language, which makes compatibility decisions unusually visible. Rejecting an operator interrupts a command someone has just typed. Changes to scope may quietly alter a function that has worked for months. fish 3.0, released December 28, addresses both kinds of behavior. It accepts && and || while revising rules that existing fish scripts depend on.
The release incorporates changes documented in the December 11 beta notes, which remain part of the final changelog. Those notes distinguish incompatible changes from deprecations and ordinary improvements. Reading only the final release's short list would miss most of the migration work for users coming from 2.7.1 or earlier.
I like fish's emphasis on a useful interactive experience without extensive configuration. That makes familiar command composition welcome, but it also makes predictable scripting rules important. The interesting question in this release is how the shell reduces everyday friction while clarifying parts of its own language.
Familiar operators preserve fish's status model
fish now accepts && as an alternative to and, || as an alternative to or, and ! as an alternative to not. The release notes present these as help for users moving from POSIX-compliant shells. The additions do not turn fish scripts into POSIX shell scripts. They provide familiar spellings for specific operations already present in fish.
The 3.0 documentation for and defines success as an exit status of zero. Its counterpart, or, runs the following command after a nonzero status. The conditional operator itself does not change the status, while the command it runs can do so.
test -f config.fish && echo "Configuration exists"
test -f config.fish || echo "Configuration is absent"
Each line stands independently. On the first line, the message runs only when test succeeds. On the second, it runs only when test fails. This is enough for many commands pasted from instructions, without requiring the user to rewrite the conditional connector before execution.
Longer chains still require attention to the status produced by each command. A failure handler following several conditional commands responds to the status that reaches it, which may come from a later command rather than the first test. Familiar punctuation does not remove that control-flow property. For a branch with several operations and a distinct alternative, an explicit conditional block can state the intended structure more clearly.
The older word forms remain useful in fish code. The release adds another spelling rather than requiring every function to change. That avoids turning an interactive convenience into unnecessary source churn for existing scripts.
Variable scope changes affect existing functions
The changelog makes loop variables no longer local to the for block. It also makes variables set in if and while conditions available outside their blocks. These are incompatible changes because code can observe a variable after a block ends when earlier behavior kept that value inside the block.
The set documentation at the release tag explains the relevant distinction. An explicit local assignment belongs to the current block. An assignment without an explicit scope reuses an existing variable's scope, or creates a function-scoped variable when no previous definition exists. Outside a function, that latter case becomes global.
Those rules mean that an unqualified assignment is not equivalent to adding --local. A function can intentionally maintain a value across several blocks, while a block-local value has a shorter lifetime. The choice belongs in the function's design, especially when a loop's control variable could share a name with another value.
Condition assignment is useful when a command's output and success determine whether to enter a block. In assignment mode, set preserves the command status, according to the versioned documentation. Its example captures the result of locating Python and uses that result as the conditional decision.
if set python_path (type -p python)
echo "Python is at $python_path"
end
The scope change makes condition-created variables available beyond the conditional block. Code that reused the same name elsewhere can therefore have different interactions after the upgrade. The correct interpretation depends on existing scope as well as the location of the assignment.
Local exported variables also become visible to functions. That closes another gap between a variable being exported and a called function being able to see it. For a function library, it means callers can influence behavior through local exported settings in cases where earlier fish versions did not expose them in the same way.
I prefer explicit scope where a value's lifetime matters to the function. That is a judgement about readability, not a claim that every assignment needs a flag. The new behavior is easier to reason about when a reader can distinguish a temporary block value from data intended to survive the block.
Arithmetic no longer launches bc
fish 3.0 replaces its math wrapper around bc with a builtin using tinyexpr. The math documentation describes evaluation without an external command. Floating-point output becomes the default, with trailing zeros trimmed, and --scale controls the output representation.
math 10 / 6
math --scale=0 10 / 6
math "sqrt(81)"
The first expression produces a fractional result. Scale zero selects integer output, so the second produces 1. Quoting the final expression protects its parentheses from shell parsing before math receives them. The documentation similarly requires quoting or escaping multiplication's asterisk when shell expansion would interpret it as a wildcard.
This is an important division of work between the shell parser and the arithmetic evaluator. The expression is an argument to a command, so shell expansion happens first. A mathematical symbol does not lose its shell meaning merely because the eventual recipient understands arithmetic.
The new evaluator does not support logical expressions. The release notes direct scripts to test for comparisons. That is a compatibility issue for scripts that used the previous bc-based behavior as a source of both calculation and boolean results. The migration involves choosing the correct command for the operation, not only removing an external dependency.
The builtin also reports failure for an invalid expression or a result that overflows, underflows, or becomes NaN. Its status is therefore a separate result from its printed number. A caller composing calculations with the new logical operators needs to distinguish successful evaluation from any interpretation of the numeric output.
Expansion becomes less surprising in specific cases
A literal {} now remains itself instead of expanding to nothing. The release notes identify find -exec as a beneficiary, since that command uses the token as a placeholder. This is a small interactive improvement with a direct language cause. fish preserves a token that another program needs to interpret.
Brace expansion also changes how successive commas behave. The documented {,,,} example produces four empty strings. Separately, literal zero indexing becomes a parser error because fish indexes begin at one. Rejecting that spelling early makes an invalid index visible before a command executes.
The older syntax for assigning several separate array-element names in one set invocation is removed. The release-tag documentation gives a slice spelling for the same intent.
set items first second third fourth
set items[1 4] replacement_one replacement_four
This updates the first and fourth elements using one variable name with multiple indices. It avoids the removed form that repeated the variable name. The change is specific to assignment syntax and should not be confused with the separate rule rejecting literal index zero.
Process and job expansion through % is largely removed, with exceptions documented for %self and job-management commands. The new $fish_pid and $last_pid variables provide alternatives for the current shell and last background process. Scripts relying on expansion therefore need to distinguish process identifiers from job specifiers understood by commands such as fg and wait.
Deprecations have a different migration status
The release introduces feature flags for staging changes, configurable at startup with fish --features or through the universal fish_features variable. Some deprecated behavior remains available in this release. That differs from removed syntax, which cannot be restored merely by treating it as a future concern.
Caret redirection is deprecated in favor of 2>, and the question-mark wildcard is also deprecated. The notes associate those changes with stderr-nocaret and qmark-noglob flags. Other migration guidance replaces IFS-based splitting in read with read --delimiter. These entries communicate an intended transition without claiming that every old behavior has already disappeared.
The same release adds private mode through fish --private. Its sessions cannot access the history file and do not persist evaluated commands for future sessions. The $fish_private_mode variable lets scripts detect the mode and adapt their own behavior. This is a defined shell-history contract, not a claim that unrelated programs cease recording their activity.
Interactive improvements remain part of the release as well. Completion search gains full fuzzy matching, abbreviations become tab-completable, and a pipe at the end of a line allows continuation on the next line. These changes reduce interruptions during command entry while the scripting changes determine how the completed command behaves.
The final release also documents a specific platform limitation. On Cygwin, 3.0.0 builds but can hang the terminal when started, and the release notes recommend continuing with 2.7.1 there. That exception matters independently of script compatibility. Elsewhere, the central migration work is to review variable lifetime and removed expansion syntax, then separate arithmetic evaluation from the comparisons now assigned to test.