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 10 and set x abc are 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 -q tests 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 3 and $a expand 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 -l gives block scope: A variable declared local inside a function, a begin/end block, a for, a while, or an if disappears at the end of that block
  • ✅ alias is sugar over function: An alias becomes a real function, so the invocation name and the body are separated instead of textually substituted
  • ✅ argparse validates options at the boundary: A function declares its own option spec, so callers do not hand-roll a getopts loop and a bad option fails before the body runs
  • ✅ Configuration lives outside scripts: ~/.config/fish/config.fish, the conf.d/*.fish snippets, 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 -l list 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 :, so PATH is edited as a list without building a string by hand
  • ⚠️ Assignment copies the list: set b $a duplicates 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 -U writes to fish_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 $status and text on stderr; there is no exception and no error value (Er1)
  • ❌ Errors are not composable: and and or chain 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 return follows it (Er3)
  • ✅ $pipestatus reports 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_found runs 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, and emit let 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 read loop

1.1.6 Platform

  • ❌ Not a POSIX shell: Fish does not implement the POSIX shell language, so a #!/bin/sh script is a different language and sh -c calls 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 sh compatibility 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_config writes 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-startup measures configuration work and --no-config skips it
  • ⚠️ Start-up runs the configuration snippets: An interactive shell sources config.fish and every file in conf.d, so a heavy prompt or completion setup costs time on each new shell
  • ⚠️ for over 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/fish by 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 sh libraries (Ec3)
  • ✅ The bundled completion system is large: complete -c declarations ship with fish and cover hundreds of commands, so everyday completion needs no add-on
  • ✅ fish_config gives 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 sh semantics

1.2.4 Domain

  • ❌ It is not a POSIX shell: A fish script runs only under fish, so portable system scripts, configure scripts, and #!/bin/sh entry 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 sh equivalent
  • ✅ Lists map to the filesystem naturally: $PATH and command output are lists of paths, and path and string split operate 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 sh or zsh: One end closes every block, set is 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 $var is not split where an sh user expects it to be
  • ⚠️ The builtin vocabulary is large: string, math, path, set, argparse, complete, and status each carry their own option set

1.2.6 Syntax

  • ✅ No word splitting means fewer quotes: $var expands as written, so a path containing a space needs no quoting to survive
  • ✅ One block terminator: end closes if, for, while, switch, and function
  • ✅ One binding form: There is no =, no setenv, and no typeset to choose between
  • ⚠️ and and or begin 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 ... end before it can be piped
  • ⚠️ switch has no fallthrough: A case ends at the next case, 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 breakpoint and fish_trace: Fish can pause with breakpoint and print a stack with status 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-execute checks syntax without running: A script can be parsed before it is executed
  • ✅ fish_indent --check fails on unformatted code: A shipped formatter gives a cheap consistency gate in a test path
  • ✅ fish --private isolates 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 $var does not split or glob, so a value with spaces or * cannot become several arguments or an unexpected path list
  • ✅ string escape and string collect make quoting explicit: Output can be quoted for re-input instead of being concatenated by hand
  • ✅ argv and argparse keep 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 -U values 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:

All published papers

References

[1]
Y. Loth, “On the nature of coupling: All coupling as shared resource access.” Zenodo, Aug. 2026. doi: 10.5281/zenodo.21925122.
[2]
Y. Loth, “Coupling as a consequence of cohesion: Decomposition, the coupling floor, and domain-imposed coupling.” Zenodo, Aug. 2026. doi: 10.5281/zenodo.21925201.
[3]
Y. Loth, “On the nature of cohesion: Cohesion as correctness relative to a partitioning rule.” Zenodo, Jul. 2026. doi: 10.5281/zenodo.21703193.
[4]
fish shell, “Fish documentation.” fish shell, 2024. Available: https://fishshell.com/docs/current/index.html
[5]
S. Joel, J. J. Wu, and F. H. Fard, “A survey on LLM-based code generation for low-resource and domain-specific programming languages,” ACM Transactions on Software Engineering and Methodology, 2025, doi: 10.1145/3770084.