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=1andx=abcare 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 -uturns 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
readonlyfixes 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
casestatement ortest(C4) - ❌ Functions share the caller’s variables: The standard provides no local declaration —
localis 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 -cstring reads its inputs as$1,$2, and so on, so a routine consumes its arguments instead of reading a global - ✅
ENVseparates 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 - ✅
unsetdrops a name: The shell reclaims the storage, so no free order and no double free can arise - ⚠️ Assignment copies the value:
b=$aduplicates 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
$?andtrap, 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 -eis not a reliable policy:errexitdoes 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
ERRtrap in the standard: Reporting and cleanup must be attached toEXIT, to a named signal, or to an explicit check after each command - ✅
trap ... EXITruns 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
- ✅
waitand$!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
- ❌
shis an implementable name, not an implementation: POSIX defines the language but not which program provides it, so/bin/shis 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
shmay lack behaviour added in Issue 8, so a script that usespipefailor$'...'is portable only to newer systems - ⚠️
sh -cis the universal entry point:system(),make, containers, andcronall use it, which makes the shell the assumed execution layer even when no script is written - ⚠️ Extensions are common and not portable:
local,[[ ... ]], arrays, andfunctionare available in manyshimplementations 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/shis 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:
dashandashreach the first command in a fraction of the time a feature-rich shell needs, which matters for the manysh -ccalls 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, orbasenameper item pays a process per iteration - ⚠️ Command substitution is buffered and re-split:
$(cmd)reads the whole output and then splits it onIFS, so a large result is walked twice - ✅ Scripts pay no start-up configuration: The
ENVfile 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, FreeBSDsh, ksh, and bash in POSIX mode all provide anshmode, 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
shby default: ShellCheck and similar tools treatshas the base case, so a minimal script is the best-supported one - ✅
checkbashismsdetects 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-aand-oprimaries 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
shmay 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
configurescripts need the one interpreter that is always present - ✅ Stop-gap automation is a native use: A two-line
sh -cavoids 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, andawkare 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
awkinvocations - ⚠️ 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 withbashorzsh - ✅ Skills transfer upward: Everything learned in
shapplies 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:
$varunquoted splits and globs, so a space or an asterisk in a value changes the command - ⚠️
[is a command, not syntax:testparses 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 - ⚠️
[andtestneed 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 withcaseor withexpr
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 -nparses a script and reports errors without running it - ✅
set -xshows 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
evalbecomes 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)
- ✅
testcan be made argument-safe:[ "X$response" = "Xexpected" ]or${var:+...}expansion removes the ambiguity that an unquoted variable introduces - ✅
set -fdisables globbing:noglobstops an expanded*from becoming a path list - ✅
umaskis 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?
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:
- 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