Ksh (kSH 93u+)
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 Ksh 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 68/100 · Engineering readiness 64/100
1.1 Core coupling
1.1.1 Type
- ❌ Values are strings with run-time attributes:
typeset -i,-F,-E,-l, and-udecide how a name is read and written, but the parser performs no static check and the attribute is enforced when the value is used (T1) - ❌ No sum types: A tagged alternative is a word plus a convention, because
typeset -Tgives a named structure rather than a choice between variants (T4) - ❌ No generics and no dispatch by type: A routine is specialised only by its positional parameters, and behaviour is selected by the string in
$0or by the command name, never by a declared type (T5, T9) - ❌ One flat global namespace:
typesetinside afunctionbody makes a name local, but a plain assignment writes the global, and no module boundary hides anything from the process (T6, T11) - ❌ An unset name reads as empty by default:
set -o nounsetturns a reference to an unset name into an error, but the option is off by default and nothing forces a variable to be initialised (T10) - ⚠️
typeset -Tdefines a real named type: A type can own discipline functions (.get,.set,.unset) and acreateroutine invoked for each instance, which is more than text binding — yet an instance is still a compound of words with no static check
1.1.2 Construction
- ❌ Global by default and mutable by default: A plain assignment writes the global namespace, and no binding is immutable unless
readonlyortypeset -ris applied (C1, C2) - ⚠️ Validation is not attached to a plain variable: A name accepts any word, and the checking that exists comes from an attribute or from a discipline function added with
typeset(C4) - ✅
functionbodies get local variables: A variable declaredtypesetin afunctionbody is local, so a routine keeps its working state out of the caller’s namespace - ✅ Discipline functions are real set and get hooks:
x.set,x.get, andx.unsetrun on assignment and reference, so a rule about a value is attached to the value’s name instead of to every write site - ✅ Name spaces and
FPATHseparate a library’s names:namespace lib { ... }keeps internal names out of the flat space, and a function onFPATHis loaded on first reference - ✅
typeset -ngives a nameref: A function can operate on a caller’s variable by name without string concatenation andeval
1.1.3 Memory
- ✅ No allocation or free in the language: There is no pointer, no address, and no deallocation builtin; the shell owns every value
- ✅ Scope and
unsetbound a lifetime: A local variable disappears when itsfunctionreturns, andunsetdrops a name, so working state does not persist by accident - ✅ Associative arrays are managed by the shell:
typeset -Aallocates and grows its own storage, so no capacity handling is written - ⚠️ Assignment copies compound values: A
typeset -Tinstance and a compound variable are copied on assignment, so a large structure is duplicated in full - ⚠️ Parsed functions stay resident: ksh93 compiles a script to an internal tree and caches it, so a long session keeps the parsed definitions in memory
- ⚠️
typeset -E,-F, and-Xchoose a representation, not a lifetime: The attribute pins a value to double, float, or long-double storage, which couples the number to a precision decision made once
1.1.4 Error
- ❌ No structured error path:
$?andtrap ... ERRare the whole apparatus, with no exception and no error value (Er1) - ❌ The ERR trap is global and undifferentiated: One handler covers the whole script, it receives no description of the failure, and it fires only where an explicit check or
set -o errexitapplies (Er3) - ❌ Errors cannot be returned: A function’s outcome is its exit status, so an error carries no payload and cannot be combined with another (Er4)
- ⚠️
set -o errexitis fragile: The option does not fire inside&&and||chains, in conditions, or after a negation, so a wrapper that suppresses it changes error behaviour - ✅
trapcovers named signals andEXIT: Cleanup can be attached toEXITand to specific signals, so a script releases resources on any exit path - ✅
$?is unambiguous at the test, andpipefailextends it: The status of the last command is read directly, andset -o pipefailmakes a mid-pipeline failure visible
1.1.5 Concurrency
- ❌ No concurrency primitives: There is no lock, channel, or await; the only parallel construct is a background job (K5)
- ✅ Coprocesses give two-way communication:
cmd |&withprint -pandread -pkeeps a child in conversation without a named pipe or a temporary file - ✅ Processes are the unit of parallelism: Concurrency is expressed with pipes and background jobs, so no shared address space has to be reasoned about
- ⚠️ Background jobs inherit the global environment: A
&job shares every global variable, the umask, and the current directory, so ordering is unspecified unless the script waits - ⚠️
waitreturns a status, not a value: The join exists, but a result must travel through a pipe, a file, or a coprocess - ⚠️ No structured concurrency: Nothing guarantees that a job started inside a function finishes before the function returns
1.1.6 Platform
- ❌ One lineage defines ksh93: The language comes from AT&T’s KornShell and its maintained fork
ksh93u+m, while the BSD-derivedpdkshandmkshimplement an older dialect, so behaviour is not interchangeable (P2) - ✅ POSIX with extensions: A script written to the POSIX shell language runs under ksh93, so it can move between systems that have a KornShell
- ⚠️ Two dialects share the name ksh:
mkshandpdkshlack associative arrays, discipline functions, and floating point, so#!/bin/kshmeans different things on different systems - ⚠️ Availability is not uniform: ksh93 ships on AIX and Solaris and is packaged for Linux, but it is not the default shell on any mainstream Linux distribution
- ✅ A manual and a test suite define the language: The ksh93 manual and the AT&T test suite, now maintained in
ksh93u+m, are the reference for behaviour - ⚠️ One implementation carries the extensions: With no standard covering them, an omission or a defect in ksh93 is a property of the language as practised
1.1.7 Trade-offs (not coupling)
- The ksh88 lineage is a separate language:
mkshandpdkshcontinue the older design on BSD and Android, and that divergence is a history of implementations rather than a coupling mechanism - Rich builtins are a deliberate choice:
print,typeset, and the.sh.*variables lengthen the learning curve to remove whole categories of external command - The floating-point attribute is a representation choice:
typeset -E,-F, and-Xselect storage precision, which is a modelling decision about the numbers a script handles - Discipline functions are a heavy mechanism: A
.gethook runs on every reference, so encapsulation is paid for with an unpredictable per-access cost namespaceis not a module system: It prevents name collisions inside one script but hides nothing from the process, which is a scope decision- POSIX mode is available but not the goal:
set -o posixnarrows ksh to the standard, and a script that wants the extensions rightly leaves it off
1.2 Ecosystem coupling
1.2.1 Performance
- ⚠️ Interpreted with an internal compile step: A script is parsed into a tree and cached, so a function body is not re-parsed on every call, but there is no artifact to precompile (R1)
- ✅ A native implementation with an optimised parser: ksh93 was designed with performance in mind, and the maintained fork continues to fix hot paths
- ✅ Coprocesses avoid repeated process creation: A child started once with
|&is driven withprint -pandread -p, so a loop does not fork per iteration - ✅ Builtins cover most text work:
print,printf,[[ ... ]], and${var/pat/rep}avoid an external process for common operations - ⚠️ Path hashing and tracked aliases are opt-in:
alias -trecords a resolved command path, so a script without it rescansPATH - ⚠️ A
.getdiscipline runs on every read: A hook on a variable adds work to each reference, so encapsulation has a per-access price
1.2.2 Ecosystem
- ❌ No package manager: There is no dependency resolver or module registry for ksh, so shared code is sourced from a directory on
FPATH(Ec1) - ⚠️ The ecosystem is smaller than Bash’s or Zsh’s: Few libraries and completions target ksh93 specifically
- ⚠️ The user base is declining: Bash is the default on Linux and absorbed most of ksh’s ideas, so new ksh93 code is rare
- ✅ The maintained fork revived the project:
ksh93u+mrestarted development and fixed defects that had stood for years - ✅ Documentation is complete: One manual documents the language, the builtins, and the discipline mechanism
- ✅ The historical influence is documented: Ksh is the source of
[[ ... ]],$'...',select,typeset,printf, andpipefailin other shells
1.2.3 Evolution
- ✅ No breaking-change culture: New features arrive as extensions, and a ksh88-style script still runs
- ✅
set -o posixpins the dialect: A script can fix POSIX behaviour so that a change in an extension does not silently alter it - ✅ The extensions are stable to the point of immobility: A construct such as
typeset -Ahas behaved the same way for decades - ⚠️ The 93v- development line was abandoned: The
ksh2020line never reached a stable release, andksh93u+mcarried the language instead - ⚠️ Development rests on a small group: The maintained fork is a volunteer effort, so the pace of change is outside user control
- ⚠️ The older dialect is frozen in place: AIX and other systems ship a ksh88-era shell, so the language as practised is fragmented
1.2.4 Domain
- ✅ Good fit for POSIX system scripting with extras: ksh93 covers portable shell code and adds associative arrays, floating point, and coprocesses where the standard stops
- ✅ Strong for text and record processing:
${var/pat/rep},${var:offset:length},[[ ... =~ ... ]], andprintfkeep string handling inside the language - ✅ Associative arrays fit lookup and configuration work:
typeset -Aand compound variables model keyed data without a temporary file or an external tool - ✅ Coprocesses suit long-running children: A child started once can be driven from a loop instead of through a named pipe
- ⚠️ Not a general-purpose language: The model is a shell over external commands, so a program with real data structures belongs elsewhere
- ⚠️ The most advanced features are not portable: Discipline functions, name spaces, and
typeset -Texist only in ksh93
1.2.5 Learning
- ✅ POSIX knowledge transfers: A Bash or
shuser can write ksh on the first day - ✅ Functions are more capable than in
sh:function name { }withtypesetlocals is a familiar and predictable form - ⚠️ The surface is large:
typesethas many attributes, and[[ ... ]],(( ... )),print,whence, and the.sh.*variables each need their own reference - ⚠️ The documentation is a specification, not a tutorial: There is little introductory material for the ksh93-specific features
- ⚠️ The dialect trap is invisible: A script written for ksh93 runs under
mkshuntil it reaches an associative array or a float
1.2.6 Syntax
- ✅ POSIX surface with familiar extensions:
if,case,for, andwhileare the standard forms, and(( ... ))and[[ ... ]]read like C - ✅
${var/pat/rep}handles substitution inline: A replacement no longer needssedin a pipeline - ⚠️
$'...'andtypesetattributes add notation: Quoting, justification, case conversion, and numeric attributes each add their own spelling - ⚠️
functionandname()differ quietly: The two definition forms have different scoping behaviour, so the choice of syntax changes the semantics - ⚠️
printandprintfoverlap: Two output builtins with different escape handling coexist
1.2.7 Testing
- ❌ No test framework in the language: A test is a script that checks
$?, with no assertion, fixture, or report facility (G1) - ❌ Faults appear only on the branch that runs: With no static check, a mistyped variable or a bad attribute is found during the run (G3)
- ✅ The DEBUG trap gives a command-by-command trace:
trap ... DEBUGruns after each command, which makes a failure locatable in a long script - ✅ A syntax check runs without execution:
ksh -nparses a script and reports errors without running it - ✅
set -o xtraceshows the expansion:set -xprints the expanded command, so an argument built from several variables can be inspected - ✅
typeset -fandwhence -vinspect the loaded program: A test can assert on a function’s definition and on how a name resolves
1.2.8 Security
- ❌ Unquoted expansion is re-parsed: A word is split and globbed again, so untrusted text can change the argument count unless it is quoted (Q2)
- ⚠️
evaland indirect expansion re-enter the interpreter: Text assembled from data and passed toeval, or a name resolved through a nameref, becomes code or a name the parser decides - ⚠️ No memory-safety defect class, but no confinement either: Values are interpreter-managed, yet the shell has no capability or sandbox model beyond its restricted mode
- ✅
[[ ... ]]does not word-split: The test construct evaluates its operands directly, so a value cannot become extra operands or a pattern - ✅ A restricted mode exists:
rksh93blockscd, redirection, and assignment toSHELL,ENV, andPATH, so an untrusted script can be run with fewer powers - ✅
readonlyandtypeset -rfreeze a value: A name can be fixed against later assignment and againstunset
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?
- ⚠️ 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 | ✅ | −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 | — | 0 | |
| Evolution subtotal | 0 | |||
| 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 | — | 0 | |
| Security subtotal | −8 |
Meta 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).
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: Ksh is a POSIX shell with the most complete set of extensions in the family, and its own constructs buy real separation: typeset gives a function local scope, discipline functions attach validation to a name, |& keeps a child in two-way conversation, and name spaces keep a library’s internals out of the flat namespace. The core score stays at 43 because the value model is still words with run-time attributes, an error is still a status code, and a plain assignment still writes the global namespace. The platform side is stronger at 68, since ksh93 is standard-compliant, well documented, and the source of many constructs other shells adopted, while its weakness is that the most advanced features exist in one implementation and the user base is shrinking.
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