Skip to content

v0.0.40

Choose a tag to compare

@blairham-release blairham-release released this 04 Nov 13:23
ab29cb2

A shell parser, interpreter and formatter in Go. One core — the common
denominator of real shells — with bash, zsh, ksh, dash and ash
as presets over a semantics vector rather than forks of one another.

This is a 0.x tag and means it. Early, and breakage is expected.
What changed since the last tag is the changelog below.

What is in the archive

Seven binaries: the substrate driver sh, the five dialect binaries, and
shfmt, which lays a script out in whichever dialect it was written in.
They install into libexec and never into a bin directory — the
shells are named after the shells they model, so a directory ahead of
/bin on PATH would answer for every program on the machine that
resolves a shell by name, and shfmt stays out of bin so that
homebrew-core's formula of the same name can still be installed.

brew install blairham/tap/sh

Where it stands

Each dialect graded against the real shell it models, on macOS against the
golden record make oracle writes, 2026-10-04, 4488 cases each:

binary against exact behavioral
bash bash 5.3.20 99.8% 99.8%
zsh zsh 5.9.2 99.8% 99.8%
dash dash 99.9% 99.96%
ksh ksh93 AJM 93u+ 99.1% 99.5%
ash BusyBox 1.37 ash 99.5% 99.6%

Exact is byte-identical stdout, stderr and status; behavioral allows a
diagnostic to be worded differently when the status and standard output
agree. Every case still short of exact is listed per dialect in
internal/oracle/testdata/conformance/ with a written reason, and CI
fails a change that adds to a list.

What is not there yet

The README's "What is not there yet" is the current list, and it is real.
The sandbox contains the shell rather than the process tree, and there is
no Windows target. Diagnostic wording is a long tail rather than the
headline gap: a bounded sample of twenty everyday error situations — a
missing command, cd onto a file, a redirect that cannot open, an unknown
option to six builtins — comes back byte-identical on standard error and
exit status for every one of the five, 100 of 100 on 2026-09-27. The probe
is internal/cmd/diagsample and takes a few seconds a column, so this line
can be re-measured rather than inherited.

Fixed

  • driver: a run of MainArgs takes its fatal-signal watch down as it returns (#7372) (28e2638)
  • driver: take a stop aimed at the shell before kill returns (#7300) (d94dd5f)
  • interp/zsh: a pipeline element keeps the parent's traps working in zsh (#7346) (8c89231)
  • interp: a bare array literal that fails to expand still makes the name an array (#7315) (83808f0), closes #7078
  • interp: a bash declaration keeps the literals it placed when its utility refuses (#7373) (89bde19)
  • interp: a bash prefix is refused word by word in written order (#7340) (e6b2478), closes #7095
  • interp: a copy's untrapped fatal signal ends a body's wait at once (#7386) (f2b5175), closes #7383
  • interp: a declaration expands its array-literal operand where it is written (#7312) (d9059f9), closes #7075
  • interp: a declaration under set -a takes the export attribute (#7303) (424f4f8)
  • interp: a failed bare literal lists and keeps the name as the shell left it (#7325) (bb6a33f), closes #7316 #7317
  • interp: a group kill at a forked body's own pid is no such process without the monitor (#7305) (75dce60)
  • interp: a ksh append to an array emptied by unset lands where ksh93 puts it (#7394) (13de311)
  • interp: a ksh call to a traced function works its prefix through again inside the body (#7336) (b4d38f6)
  • interp: a ksh declaration operand through a reference is traced under the variable it lands in (#7351) (c60c921), closes #7102
  • interp: a ksh declaration over a function's own reference stores through it (#7356) (6b35e6c), closes #7352
  • interp: a ksh element write through an element reference makes the element an array (#7363) (d3aa913)
  • interp: a ksh frozen prefix entry is refused before the declaration's operands expand (#7338) (81bbb29)
  • interp: a ksh keyed literal assigned to an element nests a table (#7396) (98ac724)
  • interp: a ksh name reference aimed at a non-name is an invalid variable name (#7330) (2b02c9f)
  • interp: a ksh persisting prefix is stored ahead of a redirection that fails (#7399) (627ca04)
  • interp: a ksh persisting prefix is stored ahead of the redirections (627ca04), closes #6986
  • interp: a ksh read through an element reference reads the nested element (#7369) (16295fb)
  • interp: a ksh reference operation resolves through the reference already there (#7359) (dc9a828)
  • interp: a ksh subscripted prefix in a command's own scope is a fresh indexed array (#7397) (b7dbba5), closes #6982
  • interp: a refused bash element write through an element reference gives up the line (#7371) (1c9f00a), closes #7367
  • interp: a trapped signal cuts a body's wait short, and a job's early signal lands after the shell goes on (#7384) (52532c4), closes #7086
  • interp: a zsh appending literal with a subscripted element stores from the first position (#7320) (7a06f95)
  • interp: a zsh subscripted literal places an empty element and refuses a negative subscript (#7385) (4f43692)
  • interp: an element's death by SIGPIPE leaves the outer PIPE handler alone (#7314) (9b52f23)
  • interp: an interactive ksh keeps a persisting prefix's entries ahead of a failed one (#7349) (251fa88), closes #7098
  • interp: answer a ( ) or $( ) copy's signal before the list goes on (#7360) (170356f)
  • interp: answer a pipeline copy's signal to its body as the wait ends (#7357) (f3078eb)
  • interp: answer a signal a child sent before it exited as the pipeline ends (#7381) (77667df)
  • interp: answer a signal from another process as the pipeline ends (#7376) (a78013a)
  • interp: answer a signal from another process before an and-or list goes on (#7368) (d56112a)
  • interp: answer a substitution's own kill $$ as its command ends (#7374) (8fa2f9e)
  • interp: bash's posix mode ends the shell on a refused subscripted prefix or reference aim (#7344) (355cba9), closes #7343
  • interp: every body around a subshell that signals the group dies of it (#7313) (139a027), closes #7245
  • interp: ksh reads an appending subscripted literal over an indexed array that holds no element (#7390) (d6febc8)
  • interp: move a gated walk's descriptors out of a park's way (#7350) (4d90b32)
  • interp: name the signal that ended a body whose EXIT trap ran (#7404) (90d6808)
  • interp: open a regular file out of a park's way (#7342) (1ee6a31)
  • interp: open a substitution's own pipe out of a park's way (#7334) (e730a82), closes #7035
  • interp: run a body's own handler as its kill returns (#7353) (1fd939a)
  • interp: under set -m a $( … )'s own number names no group (#7328) (31c64b8)
  • interp: under set -m a body forked inside a forked body leads no group (#7333) (ccfadf9)
  • interp: under set -m a pipeline element and a <(…) are the groups each dialect makes them (#7326) (267d65e)
  • interp: under set -m a subshell's group kill reaches its own job, not the shell (#7319) (8eeb3b2)
  • interp: zsh matches each word behind a declaration's assignment operand as it is expanded (#7382) (858c7a7)
  • interp: zsh traces an arithmetic command's open line, a body's declaration and an empty for initializer as zsh does (#7375) (bf3f5aa), closes #7108 #7117 #7118
  • interp: zsh writes no trace line for a modifier that runs nothing (#7348) (ffe42b5)
  • interp: zsh's declaration words outrank a same-named function, and command local declares under posixbuiltins (#7379) (65ca149), closes #7127 #7128

Performance

  • driver: open darwin's signal pipe without the signal-mask thread (#7400) (735a212), closes #6491
  • driver: the start no longer waits for the watch to take the signals the runtime drops (#7264) (59ff1e9)
  • interp: put an empty shadow over a still-plain name back in one step (#7405) (0be21bd)

Full Changelog: v0.0.39...v0.0.40