Sh (POSIX.1-2024)

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 sh 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 57/100 · Engineering readiness 64/100

1.1 Core coupling

1.1.1 Type

  • ❌ All values are strings, and numbers live in a second grammar: x=1 and x=abc are the same assignment, and $(( ... )) evaluates a signed-integer expression that is re-parsed at each use (T1)
  • ❌ No arrays and no associative arrays: A list is a string split on IFS, and the specification provides no indexed or keyed aggregate (T4)
  • ❌ No generics and no dispatch by type: A routine is specialised only by its positional parameters, and behaviour is selected by the command name (T5, T9)
  • ❌ One flat namespace: The specification gives a function no local scope, so a function’s working variables are the caller’s variables (T6, T11)
  • ❌ An unset name and an empty name are indistinguishable: set -u turns a reference to an unset name into an error, but it is off by default and an empty value still passes (T10)
  • ✅ Default and alternate expansions are part of the language: ${var:-default} and ${var:+alternate} carry the missing-value case in the expansion itself, so many ad-hoc checks disappear

1.1.2 Construction

  • ❌ Global by default and mutable by default: An assignment writes the single namespace, and only readonly fixes a value (C1, C2)
  • ❌ No constructor or validation at the point of assignment: Nothing checks a value when it is bound, so validation is written as a separate case statement or test (C4)
  • ❌ Functions share the caller’s variables: The standard provides no local declaration — local is a ksh and Bash extension, and shells that offer it differ in detail — so a helper must save and restore every name it touches, or run in a subshell
  • ✅ A subshell is a scope: ( ... ) runs in a child process, so a subroutine is isolated from the parent’s values by wrapping it
  • ✅ Positional parameters are the argument list: A function or a sh -c string reads its inputs as $1, $2, and so on, so a routine consumes its arguments instead of reading a global
  • ✅ ENV separates interactive setup from scripts: An interactive shell reads its personalisation file from $ENV, so a non-interactive script starts with none of it

1.1.3 Memory

  • ✅ No allocation or free in the language: There is no pointer and no deallocation builtin; the shell owns every string
  • ✅ A subshell releases its memory at exit: ( ... ) ends and its environment is discarded, so temporary state does not survive it
  • ✅ unset drops a name: The shell reclaims the storage, so no free order and no double free can arise
  • ⚠️ Assignment copies the value: b=$a duplicates the string, and there is no way to reference a value without copying it
  • ⚠️ A pipeline is a set of processes: The shell forks each stage, so the memory cost of a pipeline is the sum of its parts
  • ⚠️ Command substitution is buffered whole: $(cmd) collects the entire output before the enclosing command runs, so the whole result is held in memory and then split

1.1.4 Error

  • ❌ No structured error path: The entire apparatus is $? and trap, with no exception and no error value (Er1)
  • ❌ $? is lost by the next command: Any command resets it, so an interposed test discards the failure (Er3)
  • ❌ Errors do not compose: A failure is an exit status, so it cannot carry a payload, be returned as data, or be combined with another (Er4)
  • ⚠️ set -e is not a reliable policy: errexit does not fire in && and || chains, in conditions, or after a negation, so a wrapper that suppresses it changes the behaviour of the whole script
  • ⚠️ There is no ERR trap in the standard: Reporting and cleanup must be attached to EXIT, to a named signal, or to an explicit check after each command
  • ✅ trap ... EXIT runs on every exit path: Cleanup can be attached once and will run on a normal exit or on a handled signal

1.1.5 Concurrency

  • ❌ No concurrency primitives: No lock, channel, semaphore, or await exists; the only parallel construct is & (K5)
  • ✅ Pipelines are a first-class composition: The pipeline is a core operator of the language, so stages are connected without the script manipulating file descriptors
  • ✅ wait and $! make a job addressable: cmd & records the process id in $!, so a script can wait for one job rather than for all of them
  • ✅ A subshell isolates concurrent state: ( ... ) & gives a job its own copy of the environment, so the job cannot change the parent’s values
  • ⚠️ Background jobs share files and the working directory: A & job inherits the environment, the umask, and the current directory, so ordering is unspecified unless the script waits
  • ⚠️ No value comes back from a job: A status arrives through wait, and a result must travel through a file, a pipe, or a named pipe

1.1.6 Platform

  • ❌ sh is an implementable name, not an implementation: POSIX defines the language but not which program provides it, so /bin/sh is dash, ash, bash in POSIX mode, or a vendor shell depending on the system, and only the standard subset is portable (P2)
  • ✅ POSIX guarantees that a shell exists: Every conforming system provides sh, so a script invokes an interpreter without installing anything
  • ✅ The standard is written for maximum reach: The language contains no optional syntax that a conforming shell may omit
  • ⚠️ Implementations are not equally complete: An older sh may lack behaviour added in Issue 8, so a script that uses pipefail or $'...' is portable only to newer systems
  • ⚠️ sh -c is the universal entry point: system(), make, containers, and cron all use it, which makes the shell the assumed execution layer even when no script is written
  • ⚠️ Extensions are common and not portable: local, [[ ... ]], arrays, and function are available in many sh implementations but are outside the standard

1.1.7 Trade-offs (not coupling)

  • Minimalism is the point: The language is small because every construct must be implementable on every conforming system, so the absence of arrays is a scope decision
  • Portability costs expressiveness: Writing to the standard means doing without associative arrays, pattern-replacement expansions, and floating point
  • No interactive features is a deliberate omission: Line editing, history, and completion are terminal concerns and are kept out of the standard
  • /bin/sh is a policy choice on each system: A distribution links it to dash, bash, or another shell, and that choice is not the language’s
  • Integer arithmetic is the standard’s floor: A script that needs floating point calls an external tool, which is a design boundary rather than a defect
  • The specification is written for implementers: Its precision makes it slow to read, and that is the price of one text that many shells obey

1.2 Ecosystem coupling

1.2.1 Performance

  • ⚠️ Interpreted with no compilation: Each command is parsed and executed as text, and there is no artifact and no JIT (R1)
  • ✅ A minimal implementation starts fast: dash and ash reach the first command in a fraction of the time a feature-rich shell needs, which matters for the many sh -c calls in a build or a boot
  • ✅ Builtins replace processes for common work: printf, read, test, getopts, and parameter expansion avoid an external command
  • ⚠️ Every external command forks: The builtin set is small, so a loop that calls sed, grep, or basename per item pays a process per iteration
  • ⚠️ Command substitution is buffered and re-split: $(cmd) reads the whole output and then splits it on IFS, so a large result is walked twice
  • ✅ Scripts pay no start-up configuration: The ENV file is read only by an interactive shell, so a non-interactive script pays nothing for user setup

1.2.2 Ecosystem

  • ❌ No package manager: There is no dependency mechanism for shell code, so shared routines are sourced from a directory or vendored (Ec1)
  • ✅ Every UNIX-like system has an implementation: Dash, ash and BusyBox ash, FreeBSD sh, ksh, and bash in POSIX mode all provide an sh mode, so a script always has a host
  • ✅ The standard is published and maintained: The Open Group Base Specifications define the language in one stable document with a change history
  • ✅ Analysers target sh by default: ShellCheck and similar tools treat sh as the base case, so a minimal script is the best-supported one
  • ✅ checkbashisms detects non-portable constructs: A script can be checked against the POSIX surface before it is released
  • ⚠️ Most published shell libraries assume Bash: They use arrays and [[ ... ]], so they cannot be used in a strictly conforming script

1.2.3 Evolution

  • ❌ The standard moves slowly: Issue 7 appeared in 2018 and Issue 8 in 2024, so a needed feature waits years for the committee (V4)
  • ✅ The standard is backward compatible: Issue 8 adds features and removes only what was already marked obsolescent, so old scripts keep running
  • ✅ Removals are signposted for years: test’s -a and -o primaries were marked obsolescent in Issue 7 before Issue 8 removed them
  • ✅ The change history ships with the text: Every change from the previous issue is recorded in the specification rationale
  • ⚠️ Adoption lags publication: A shipped sh may implement an older issue for years, so a script written to Issue 8 may not run on the host it is deployed to
  • ⚠️ The committee decides on defect reports: A change enters the standard only through an accepted interpretation, so the process is deliberate rather than responsive

1.2.4 Domain

  • ✅ Excellent fit for its intended domain: Boot scripts, installers, container entrypoints, and configure scripts need the one interpreter that is always present
  • ✅ Stop-gap automation is a native use: A two-line sh -c avoids a build step and a binary, which is why build tools, cron, and orchestrators embed it
  • ✅ Portable text filtering composes well: Pipelines over grep, sed, and awk are the idiom the language was designed around
  • ✅ Configuration and environment setup read cleanly: Exporting variables, setting umask, and changing directories is a few lines
  • ⚠️ Data structures push the script elsewhere: Without arrays, a keyed or indexed collection becomes a temporary file and a series of awk invocations
  • ⚠️ Not a general-purpose language: The model is a shell over external commands, so a program with real logic belongs in another language

1.2.5 Learning

  • ✅ A small surface to learn: Assignments, if, case, and pipelines cover most scripts, and the standard is short compared with bash or zsh
  • ✅ Skills transfer upward: Everything learned in sh applies to Bash, ksh, and zsh, which is why it is the usual starting point
  • ✅ The specification is the reference: A precise description of every construct exists in one place
  • ⚠️ Quoting and word splitting trap newcomers: $var unquoted splits and globs, so a space or an asterisk in a value changes the command
  • ⚠️ [ is a command, not syntax: test parses its arguments, so an unquoted empty variable changes the argument count and can turn a test into an error or a truth
  • ⚠️ The specification is written for implementers: It is precise about behaviour and thin on explanation, so learners need other material

1.2.6 Syntax

  • ✅ The language is small and predictable: Assignments, if, while, for, case, and pipelines cover the whole grammar
  • ✅ Here-documents handle multi-line input: << and <<- embed a block of text in a script without escaping
  • ⚠️ Expansion has five forms to remember: %, %%, #, ##, and ${#var} are compact but each has its own rule
  • ⚠️ Quoting rules are context-dependent: Single quotes, double quotes, and backslash escapes each treat $, backtick, and newline differently
  • ⚠️ [ and test need the operands spaced out: The bracket form requires a space after [ and before ], which is easy to forget
  • ⚠️ No [[ ... ]] in the standard: The construct was removed from the shell language description in an early POSIX proposal, so richer tests are written with case or with expr

1.2.7 Testing

  • ❌ No test framework in the language: A test is a script that checks $?, with no assertion, fixture, or reporting facility (G1)
  • ❌ Faults appear only on the branch that runs: With no static check, a mistyped variable or a missing file is found during the run (G3)
  • ✅ A syntax check runs without execution: sh -n parses a script and reports errors without running it
  • ✅ set -x shows the expansion: The traced command reveals how a value was split and quoted before it ran
  • ✅ Exit status is a reliable oracle: $? after a command gives one unambiguous value to assert on
  • ✅ A test can be isolated with a subshell: ( ... ) gives each case a fresh copy of the environment, so one case cannot leak into the next

1.2.8 Security

  • ❌ Command construction from text is the classic injection path: A value expanded into a command line or passed to eval becomes code, so unquoted expansion of untrusted input is unsafe (Q3)
  • ❌ Weak typing removes a check the shell cannot make: A value that should be a number, a path, or a flag is a string, and only the command that receives it can object (Q2)
  • ✅ test can be made argument-safe: [ "X$response" = "Xexpected" ] or ${var:+...} expansion removes the ambiguity that an unquoted variable introduces
  • ✅ set -f disables globbing: noglob stops an expanded * from becoming a path list
  • ✅ umask is part of the language: File-creation permissions are set inside the script rather than left to the ambient environment
  • ✅ No memory-safety surface: Strings are interpreter-managed, so a script has no buffer to overflow

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 fast in-editor analysis.

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?

  • ✅ A large training corpus and strong code-generation results: models write this language well [5], [6].

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 ✅ −4
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 (−59)
Construction C1 global mutable state by default ✅ −15
C2 no immutability by default ✅ −4
C3 constructors coupled to allocation — 0
C4 no constructor/validation ✅ −4
Construction subtotal −15 (−23)
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 — 0
Ec4 proprietary — 0
Ec5 platform-locked tooling — 0
Ec6 monoculture — 0
Ec7 legacy burden — 0
Ecosystem subtotal −4
Evolution V1 breaking changes — 0
V2 frozen — 0
V3 discontinued — 0
V4 glacial standards ✅ −4
Evolution subtotal −4
Domain D1 domain/vendor lock-in — 0
D2 poor domain fit — 0
D3 framework forces architecture — 0
Domain subtotal 0
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 — 0
G3 runtime-only errors ✅ −4
Testing subtotal −12
Security Q1 memory-unsafe — 0
Q2 dynamic/weak typing risk ✅ −8
Q3 known vulnerability/injection ✅ −8
Security subtotal −15 (−16)

Meta Dimensions score: 57/100 — total cost = −43 (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 ✅ −4
Feedback subtotal −4
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: 64/100 — total cost = −36 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).

Verdict: sh is the smallest language in the shell family, and its coupling profile is the family floor: every value is a string, one namespace holds every name, a function has no private scope, and an error is an exit status rather than a value. What it returns for that price is reach — one text that every conforming system implements, so a script runs wherever POSIX is present, and its standard is slow to change but never breaks what it already promised. The platform and ecosystem score of 57 reflects that trade: the tooling and the specification are complete and stable, while the glacial standardisation cycle and the missing test and error apparatus remain real costs.


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]
IEEE and The Open Group, “Standard for information technology — portable operating system interface (POSIX), base specifications, issue 8.” IEEE; IEEE Std 1003.1-2024, New York, NY, 2024.
[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.
[6]
Y. Qing et al., “EffiBench-x: A multi-language benchmark for measuring efficiency of LLM-generated code,” in Advances in neural information processing systems 38 (NeurIPS), datasets and benchmarks track, 2025. doi: 10.48550/arXiv.2505.13004.