Skip to content

v0.0.36

Choose a tag to compare

@github-actions github-actions released this 17 Oct 02:55
· 249 commits to main since this release
v0.0.36
8d734ec

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, over 4488 cases —
make conformance-dialects:

binary against exact behavioral
bash bash 5.3.20 98% 100%
zsh zsh 5.9.2 98% 99%
dash dash 98% 100%
ksh ksh93 AJM 93u+ 96% 99%
ash BusyBox 1.37 ash 93% 99%

Exact is byte-identical stdout, stderr and status; behavioral allows a
diagnostic to be worded differently when the status and output agree. The
distance between the two columns is almost entirely message wording.

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.

Changelog

Dialects

  • ash: a backquoted body starts on the line above its opener (#6894)
  • bash, ksh: trace a redirection target's process substitution body (#6885)
  • bash: $BASH_COMMAND lays out a command substitution's body (#6877)
  • bash: a backgrounded pipeline fires the DEBUG trap in the shell that starts it (#6850)
  • bash: a move's '-' must be written, and a {name} dup refusal names the name (#6875)
  • dash, ksh, ash: a ( … ) at the tail of a -c string is the shell, and its last program replaces it (#6856)
  • ksh, zsh: a base declared on RANDOM, LINENO or SECONDS is listed, and read in zsh (#6855)
  • ksh: a chained element literal nests at the end of its chain; trace a negative subscript as its position (#6879)
  • ksh: a persisting prefix makes a typed name a plain scalar; a keyword call's cell has no letters (#6860)
  • ksh: a process substitution is a word of its own (#6867)
  • ksh: command before a builtin takes the export off, and a case letter keeps it (#6862)
  • ksh: name the element in three frozen-name refusals, and drop read -A's warning (#6834)
  • ksh: quotes in an arithmetic subscript: scanned, removed, and traced as ksh93 does (#6891)
  • ksh: read -A into a table's key nests; a counted read fills its first name whole (#6870)
  • ksh: three parse refusals named the way ksh93 names them (#6872)
  • zsh, ksh: an external command's prefix is expanded in its child (#6861)
  • zsh, ksh: getopts leaves an unset OPTIND unset and keeps its own place (#6842)
  • zsh: a declaration number with no name is a listing, and local's letters filter (#6831)
  • zsh: a declaration's store to 0 sets $0, and a literal's sentence follows the route (#6840)
  • zsh: a function-local EXIT trap skips a give-up and reads 0 after an exit (#6874)
  • zsh: a parse failure quotes the script visibly in its locale (#6897)
  • zsh: a refused set option is an error condition to an always block (#6863)
  • zsh: control bytes in arithmetic, a word after a then-body group, unknown prompt escapes (#6846)
  • zsh: ignorebraces takes the {name} redirection away (#6893)
  • zsh: refuse a process substitution against a plain redirection, or with more after it (#6858)
  • zsh: the M and T glob qualifiers, and markdirs marks what e is shown (#6888)
  • zsh: unset drops a script binding's hide word, and leaves a detached tie's other half (#6841)
  • zsh: zmodload refuses a module name by its spelling before loading it (#6844)

The interactive shell

  • repl: a return typed at a prompt ends the line or the session, never neither (#6832)

The language and the interpreter

  • interp, driver: an interactive shell ignores TERM, from itself or from outside (#6845)
  • interp: a declaration operand's = is looked for after its subscript (#6895)
  • interp: a frozen array's element is not written by (( )), let, getopts or ${a[k]:=} (#6848)
  • interp: a frozen element or list written through a reference names the reference (#6886)
  • interp: a positional index too wide for any list is past the end (#6880)
  • interp: a refused subscript quotes its command substitutions laid out (#6865)
  • interp: a[@]=(…) and a[*]=(…) answer the whole-array fields, not the element roads (#6827)
  • interp: an out-of-range or empty subscript costs the command list, not a -c string (#6853)
  • interp: ksh saves the streams a redirection list replaces, except for its last command (#6837)
  • interp: the nonblocking-open test opens a substitution through PathToOpen (#6884)
  • interp: three per-command costs off the counting loop (#6496) (#6839)

Other

  • arith: word a group's leftover byte the way the panel splits it (#6830)
  • arithmetic: a value that will not evaluate is quoted with its own blanks (#6851)
  • build: compile every shell with a profile of what a shell spends its time on (#6496) (#6864)
  • case: an arm matching nothing is 0, whatever a pattern's substitution left (#6843)
  • deps: take golang.org/x/text v0.42.0 for GO-2026-6629 (#6881)
  • gated: a /bin/sh for distroless images that cannot exec, promoted from localiam (#6892)

Full Changelog: v0.0.35...v0.0.36