Fish (4.0)
This assessment applies the coupling account — coupling as access to a shared resource, and the coupling floor [1], [2] — and the knowledge-embodiment theorem [3]; the language is assessed against its official reference [4].
1 Assessment
This page assesses Fish in five parts: core coupling — what the language’s own constructs force; ecosystem coupling — what the surrounding platform, tooling, and community cost; engineering readiness — whether the artifact can be reproduced, verified, observed, worked in quickly, connected to other languages, and governed openly; the profile — the unscored facts a score cannot express; and the scoring detail — the mechanism marks and the arithmetic behind every score. A ✅ bullet marks a construct that helps. A ⚠️ or ❌ bullet marks a cost; the scoring model does not price every cost named here.
Core coupling 43/100 · Ecosystem coupling 49/100 · Engineering readiness 68/100
1.1 Core coupling
1.1.1 Type
- ❌ Values are strings with no declaration:
set x 10andset x abcare the same operation, and nothing states what a name holds (T1) - ❌ No sum types: An option that may be absent, numeric, or textual is a list of words, and the reader decodes the convention from the calling code (T4)
- ❌ No generics: A function is specialised only by its positional arguments, so the same logic is written again for each shape of data (T5)
- ❌ One namespace for every name: Functions and variables share one flat space, so collisions are avoided by convention — the
__fish_-prefixed helper names in the bundled completions are the visible result (T6, T11) - ❌ An unset name expands to an empty list:
set -qtests whether a name is set, but nothing forces it to be, so a mistyped variable silently expands to nothing (T10) - ✅ Lists are values, not strings to be re-split:
set a 1 2 3and$aexpand to exactly three arguments, so element structure survives expansion without quoting
1.1.2 Construction
- ❌ Global is still the default: An assignment without a scope flag reaches the innermost existing scope or the global one, so an accidental write into the caller is easy (C1)
- ❌ No immutability by default: There is no read-only binding, so any visible name can be reassigned (C2)
- ✅
set -lgives block scope: A variable declared local inside a function, abegin/endblock, afor, awhile, or anifdisappears at the end of that block - ✅
aliasis sugar overfunction: An alias becomes a real function, so the invocation name and the body are separated instead of textually substituted - ✅
argparsevalidates options at the boundary: A function declares its own option spec, so callers do not hand-roll agetoptsloop and a bad option fails before the body runs - ✅ Configuration lives outside scripts:
~/.config/fish/config.fish, theconf.d/*.fishsnippets, and the universal-variable file separate shell setup from script logic
1.1.3 Memory
- ✅ No allocation syntax: There is no pointer, no address, and no
free; lists and strings are allocated by the interpreter - ✅ Block scope bounds a value’s lifetime: A
set -llist is released at the end of its block, so storage does not leak past the code that created it - ✅ Export joins a list on the way out: A list assigned to an exported variable is joined with
:, soPATHis edited as a list without building a string by hand - ⚠️ Assignment copies the list:
set b $aduplicates every element, and there is no way to reference a list without copying it - ⚠️ Command substitution is materialised whole:
(cmd)builds the entire output before the enclosing command runs, so a large output is held in memory - ⚠️ Universal variables outlive the process:
set -Uwrites tofish_variables, so a value persists after the shell exits until it is erased deliberately
1.1.4 Error
- ❌ No structured error path: A command reports success through
$statusand text on stderr; there is no exception and no error value (Er1) - ❌ Errors are not composable:
andandorchain status codes, but a status carries no payload and cannot be returned from a function as data (Er4) - ❌ A non-zero status is easy to ignore: A function reports the status of its last command, so a failure in the middle of a block is discarded unless
or returnfollows it (Er3) - ✅
$pipestatusreports every stage: Each stage of a pipeline keeps its own status, so a failure is not hidden by a successful last stage - ⚠️ Syntax errors are reported with a source position: Fish names the file, line, and column, but only for text it has parsed, so a fault inside a function body is reported when the function is defined
- ⚠️ An unknown command is a hookable event:
fish_command_not_foundruns when a command is missing, so the message can be replaced, but the failure remains a status code
1.1.5 Concurrency
- ❌ No concurrency primitives: There is no thread, lock, channel, or await; the only parallel construct is backgrounding with
&(K5) - ✅ Event handlers decouple producer from consumer:
function --on-variable NAME,--on-event,--on-process-exit, andemitlet a consumer subscribe without the producer knowing it exists - ✅ Processes are the unit of parallelism: Concurrency is expressed with pipes and background jobs, so no shared address space has to be reasoned about
- ⚠️ Process events are the only synchronisation: A script coordinates jobs by watching exit events or by
wait, and no value can be returned from a job - ⚠️ Background jobs share the environment: A
&job inherits every global and universal variable, so ordering between jobs is unspecified unless the script waits - ⚠️ No coprocess: Fish cannot keep a two-way conversation with a child, so an interactive child must be driven through files or a
while readloop
1.1.6 Platform
- ❌ Not a POSIX shell: Fish does not implement the POSIX shell language, so a
#!/bin/shscript is a different language andsh -ccalls a separate program (P2) - ✅ One implementation defines the language: With no standard to satisfy, behaviour is fixed by the fish manual and its release notes
- ✅ Runs on every Unix-like target: Fish ships on Linux, the BSDs, and macOS, and is packaged by every major distribution
- ⚠️ Windows support is through an emulation layer: Fish relies on Unix APIs and their semantics, so Windows use goes through WSL, Cygwin, or MSYS2 rather than a native port
- ⚠️ One implementation is one point of failure: There is no second implementation to test the language’s contract against
- ⚠️ fish is the default login shell nowhere: A user installs it and sets it as their shell, so availability depends on the host’s configuration
1.1.7 Trade-offs (not coupling)
- Fish is deliberately not POSIX: Giving up
shcompatibility buys the design freedom that removes word splitting and most quoting noise - Universal variables are a convenience with a lifecycle cost: Persisting a value outside the shell trades reproducibility for ergonomics
- Aliases became functions because functions are better: The change breaks the textual-substitution habit from other shells, which is the point of the change
- The web-based configuration UI is a front end:
fish_configwrites plain files under~/.config/fish, so the browser is not a runtime dependency - Autosuggestions and highlighting are interactive features: They cost start-up work and terminal capability, and they are not part of the scripting language
- The Rust port is an implementation change: The language contract is unchanged; the gain is in the codebase, not in what a script may rely on
1.2 Ecosystem coupling
1.2.1 Performance
- ⚠️ Interpreted with no compilation: Every command is parsed and executed as text, and fish offers no ahead-of-time path (R1)
- ✅ The core is native code: Fish was written in C++ through the 3.x series and was ported to Rust for 4.0, so parsing, expansion, and completion run in compiled code rather than in script
- ✅ Command substitution does not re-split:
(cmd)performs no word splitting or globbing, so a large result is not re-parsed argument by argument - ✅ Start-up can be profiled and trimmed:
fish --profile-startupmeasures configuration work and--no-configskips it - ⚠️ Start-up runs the configuration snippets: An interactive shell sources
config.fishand every file inconf.d, so a heavy prompt or completion setup costs time on each new shell - ⚠️
forover a substitution materialises the output: A large(cmd)result is held in memory before the loop starts
1.2.2 Ecosystem
- ❌ No package manager in the language: There is no dependency resolver, so plugins are copied into
~/.config/fishby third-party managers such as Fisher or Oh My Fish (Ec1) - ⚠️ The ecosystem is small but growing: Completions and prompt themes exist for popular tools, and there are far fewer fish libraries than
shlibraries (Ec3) - ✅ The bundled completion system is large:
complete -cdeclarations ship with fish and cover hundreds of commands, so everyday completion needs no add-on - ✅
fish_configgives a built-in UI: Prompts and themes are edited in a browser against files under~/.config/fish - ✅ Documentation is first-class: The manual, the tutorial, and
help <builtin>are maintained with each release - ⚠️ Third-party plugin managers own the plugin ecosystem: Fisher and Oh My Fish are unofficial, so the plugin supply chain is not part of the language
1.2.3 Evolution
- ✅ No breaking-change culture: Fish adds features and documents the few incompatible changes in the release notes
- ✅ Release notes record every incompatibility: The manual lists removals and behaviour changes for each version, so an upgrade is inspectable
- ✅ The project ships on a regular cadence: Fish 4.0 was released in February 2025, and further 4.x releases followed it
- ⚠️ No standards body: The direction of the language follows its maintainers rather than a committee
- ⚠️ Non-POSIX means the language may keep diverging: With no standard to align to, fish is free to change and is not compelled to hold
shsemantics
1.2.4 Domain
- ❌ It is not a POSIX shell: A fish script runs only under fish, so portable system scripts,
configurescripts, and#!/bin/shentry points cannot be written in fish (D2) - ✅ Strong for interactive work: Syntax highlighting, autosuggestions, and completion make the terminal the primary domain
- ✅ Good for personal automation on a fixed host: Where the interpreter is guaranteed, a fish script is shorter and clearer than its
shequivalent - ✅ Lists map to the filesystem naturally:
$PATHand command output are lists of paths, andpathandstring splitoperate on them directly - ⚠️ Weak for distribution: A fish script cannot be shipped to a host without fish, which rules fish out for bootstrap, packaging, and installer code
- ⚠️ Not a general-purpose language: The model is a shell over external commands, so real data processing moves to other tools
1.2.5 Learning
- ✅ The tutorial and manual are written for newcomers: Fish ships a tutorial and an FAQ, and each builtin has its own help page
- ✅ Error messages name the file, line, and column: A beginner sees where the problem is rather than a bare failure
- ✅ The language is smaller and more regular than
shorzsh: Oneendcloses every block,setis the only binding command, and there is no word splitting to learn - ⚠️ Quoting habits from other shells misfire:
"$var"and[ -f x ]do not apply, and$varis not split where anshuser expects it to be - ⚠️ The builtin vocabulary is large:
string,math,path,set,argparse,complete, andstatuseach carry their own option set
1.2.6 Syntax
- ✅ No word splitting means fewer quotes:
$varexpands as written, so a path containing a space needs no quoting to survive - ✅ One block terminator:
endclosesif,for,while,switch, andfunction - ✅ One binding form: There is no
=, nosetenv, and notypesetto choose between - ⚠️
andandorbegin a line: A boolean continuation puts the operator at the start of the next command, which reads unusually - ⚠️ Piping a block needs a wrapper: A multi-command producer must be written as
begin ... endbefore it can be piped - ⚠️
switchhas no fallthrough: A case ends at the nextcase, so an intentional fallthrough must be written as a second case
1.2.7 Testing
- ❌ No test framework in the language: A test is a function that calls the code and checks
$status, with no assertion or reporting facility (G1) - ❌ Debugging is
breakpointandfish_trace: Fish can pause withbreakpointand print a stack withstatus stack-trace, but it has no debugger with breakpoints, stepping, and inspection (G2) - ❌ Faults appear only on the branch that runs: With no static check, a mistyped variable or a bad list index is found during the run (G3)
- ✅
fish --no-executechecks syntax without running: A script can be parsed before it is executed - ✅
fish_indent --checkfails on unformatted code: A shipped formatter gives a cheap consistency gate in a test path - ✅
fish --privateisolates a test from user state: Configuration, history, and universal variables are left untouched, so a test run is reproducible
1.2.8 Security
- ❌ Text can re-enter the interpreter: Input assembled from untrusted data and passed to
eval, or interpolated into a substituted command, becomes code (Q3) - ✅ No word splitting removes a whole class of injection: An unquoted
$vardoes not split or glob, so a value with spaces or*cannot become several arguments or an unexpected path list - ✅
string escapeandstring collectmake quoting explicit: Output can be quoted for re-input instead of being concatenated by hand - ✅
argvandargparsekeep options out of the parser: A function receives its arguments as a list, so a value beginning with-is not silently taken as an option - ✅ No memory-safety surface: Lists and strings are interpreter-managed, so a fish script has no buffer to overflow
- ⚠️ History and universal variables are unencrypted state: Command history and
set -Uvalues are written to disk in the user’s configuration, so a secret typed at the prompt is recorded by default
1.3 Engineering readiness
1.3.1 Reproducibility
- ✅ No charged mechanism in this dimension.
1.3.2 Supply chain
- ⚠️ Published packages carry no signed provenance by default.
1.3.3 Observability
- ⚠️ No stable tracing and metrics standard.
- ⚠️ The standard library has no structured logging.
1.3.4 Feedback
- ✅ No charged mechanism in this dimension.
1.3.5 Interop
- ❌ No general foreign-function interface.
- ⚠️ No maintained portable target.
1.3.6 Governance
- ✅ No charged mechanism in this dimension.
1.4 Profile
Unscored, and kept separate from every score on purpose. The profile records what a mechanism price cannot express: what a running program costs, how well a coding model and an agent work in the language, and whether a safety-critical path exists.
1.4.1 Energy and cost
What this axis asks: what does a running program cost in energy and time?
- ⚠️ Every operation starts a process: time, and therefore energy, is dominated by process startup.
1.4.2 AI and agent readiness
What this axis asks: how well does a coding model and an agent work in this language?
- ⚠️ Moderate training data: models write it competently, with measurable language-specific weakness [5].
1.4.3 Safety certification
What this axis asks: is there a certified or standards-qualified path to a safety-critical system?
- ⚠️ No certified or standards-qualified path for safety-critical systems.
1.5 Scoring detail
1.5.1 Core coupling Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Type | T1 |
dynamic no static check | ✅ | −15 |
T2 |
weak static | — | 0 | |
T3 |
dynamic/duck dispatch | — | 0 | |
T4 |
no sum types | ✅ | −4 | |
T5 |
no generics | ✅ | −4 | |
T6 |
no namespaces | ✅ | −8 | |
T7 |
type erasure/reflection | — | 0 | |
T8 |
operator overloading | — | 0 | |
T9 |
inheritance required for polymorphism | — | 0 | |
T10 |
no null-safety | ✅ | −8 | |
T11 |
no module boundary | ✅ | −8 | |
T12 |
no effect tracking | ✅ | −8 | |
T13 |
async/sync colouring | — | 0 | |
T14 |
unchecked foreign boundary | — | 0 | |
| Type subtotal | −15 (−55) | |||
| Construction | C1 |
global mutable state by default | ✅ | −15 |
C2 |
no immutability by default | ✅ | −4 | |
C3 |
constructors coupled to allocation | — | 0 | |
C4 |
no constructor/validation | — | 0 | |
| Construction subtotal | −15 (−19) | |||
| Memory | M1 |
manual deallocation | — | 0 |
M2 |
GC non-deterministic pauses | — | 0 | |
M3 |
no RAII | — | 0 | |
M4 |
no ownership model | — | 0 | |
| Memory subtotal | 0 | |||
| Error | Er1 |
no structured path | ✅ | −15 |
Er2 |
unchecked exceptions only | — | 0 | |
Er3 |
errors ignored/non-exhaustive | ✅ | −4 | |
Er4 |
no composable result type | ✅ | −4 | |
| Error subtotal | −15 (−23) | |||
| Concurrency | K1 |
GIL/no parallel | — | 0 |
K2 |
shared mutable state default | — | 0 | |
K3 |
manual sync no race detection | — | 0 | |
K4 |
no structured concurrency | — | 0 | |
K5 |
no primitives | ✅ | −4 | |
| Concurrency subtotal | −4 | |||
| Platform | P1 |
cannot function outside one vendor/domain | — | 0 |
P2 |
single-vendor but other uses | ✅ | −8 | |
P3 |
legacy/discontinued platform | — | 0 | |
| Platform subtotal | −8 |
Core Dimensions score: 43/100 — total cost = −57 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
1.5.2 Ecosystem coupling Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Performance | R1 |
interpreted no AOT/JIT | ✅ | −8 |
R2 |
GC pauses | — | 0 | |
R3 |
JIT warmup | — | 0 | |
| Performance subtotal | −8 | |||
| Ecosystem | Ec1 |
no package manager | ✅ | −4 |
Ec2 |
fragmented tooling | — | 0 | |
Ec3 |
small/niche/discontinued | ✅ | −8 | |
Ec4 |
proprietary | — | 0 | |
Ec5 |
platform-locked tooling | — | 0 | |
Ec6 |
monoculture | — | 0 | |
Ec7 |
legacy burden | — | 0 | |
| Ecosystem subtotal | −12 | |||
| Evolution | V1 |
breaking changes | — | 0 |
V2 |
frozen | — | 0 | |
V3 |
discontinued | — | 0 | |
V4 |
glacial standards | — | 0 | |
| Evolution subtotal | 0 | |||
| Domain | D1 |
domain/vendor lock-in | — | 0 |
D2 |
poor domain fit | ✅ | −8 | |
D3 |
framework forces architecture | — | 0 | |
| Domain subtotal | −8 | |||
| Learning | L1 |
steep | — | 0 |
L2 |
large surface/multi-paradigm | — | 0 | |
L3 |
sparse docs | — | 0 | |
| Learning subtotal | 0 | |||
| Syntax | S1 |
verbose | — | 0 |
S2 |
inconsistent | — | 0 | |
S3 |
punctuation-heavy | — | 0 | |
| Syntax subtotal | 0 | |||
| Testing | G1 |
weak tooling | ✅ | −8 |
G2 |
poor debugging | ✅ | −8 | |
G3 |
runtime-only errors | ✅ | −4 | |
| Testing subtotal | −15 (−20) | |||
| Security | Q1 |
memory-unsafe | — | 0 |
Q2 |
dynamic/weak typing risk | — | 0 | |
Q3 |
known vulnerability/injection | ✅ | −8 | |
| Security subtotal | −8 |
Meta Dimensions score: 49/100 — total cost = −51 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
1.5.3 Engineering readiness Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Reproducibility | Y1 |
build not reproducible | — | 0 |
Y2 |
dependencies not pinned | — | 0 | |
| Reproducibility subtotal | 0 | |||
| Supply chain | Y3 |
no signed provenance | ✅ | −8 |
Y4 |
registry without namespacing | — | 0 | |
| Supply chain subtotal | −8 | |||
| Observability | Y5 |
no stable tracing standard | ✅ | −8 |
Y6 |
no structured logging | ✅ | −4 | |
| Observability subtotal | −12 | |||
| Feedback | Y7 |
slow build | — | 0 |
Y8 |
no fast in-editor analysis | — | 0 | |
| Feedback subtotal | 0 | |||
| Interop | Y9 |
no foreign-function interface | ✅ | −8 |
Y10 |
no portable target | ✅ | −4 | |
| Interop subtotal | −12 | |||
| Governance | Y11 |
single-vendor governance | — | 0 |
Y12 |
closed specification | — | 0 | |
| Governance subtotal | 0 |
Readiness Dimensions score: 68/100 — total cost = −32 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
Verdict: Fish lowers the coupling that a scripting shell normally imposes on construction and on text handling: set -l gives block scope, $var never splits, lists are first-class, and functions replace textual aliases. Its core score still stops at 43, because the value model has no types, every name is global by default, and an error remains a status code rather than a value. The price is paid elsewhere: fish is not a POSIX shell, so a fish script runs only where fish is installed, and start-up configuration, an unofficial plugin supply chain, and the absence of a test framework hold the platform and ecosystem score to 49.
The formal development. The definitions, theorems, and proofs behind this page are published in:
- On the Nature of Coupling: All Coupling as Shared Resource Access — Zenodo, 2026 — doi:10.5281/zenodo.21925122
- Coupling as a Consequence of Cohesion: Decomposition, the Coupling Floor, and Domain-Imposed Coupling — Zenodo, 2026 — doi:10.5281/zenodo.21925201
- On the Nature of Cohesion: Cohesion as Correctness Relative to a Partitioning Rule — Zenodo, 2026 — doi:10.5281/zenodo.21703193