Skip to content

v0.0.35

Choose a tag to compare

@github-actions github-actions released this 16 Oct 12:14
· 295 commits to main since this release
v0.0.35
7af7695

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 function keeps the line count of the text it was written in (#6796)
  • ash: locate an interactive -c string as the prompt (#6713)
  • ash: locate and end an interactive script as BusyBox does (#6804)
  • ash: name no line for eval's text in an interactive shell (#6697)
  • bash, ksh, zsh: store an array-literal prefix as each shell does (#6635)
  • bash, ksh: a {name} store is seen through the command's own prefix (#6699)
  • bash: a move's source, a duplication's name, and a bare {name} list (#6748)
  • bash: a typed value that will not evaluate gives up the line, not the script (#6708)
  • bash: refuse each frozen prefix entry where the walk reaches it (#6601)
  • dash, ash: set -- and shift restart the getopts scan (#6653) (#6823)
  • dash: refuse a non-number stored into OPTIND, on the freeze's route (#6574)
  • dialect: measure array-operation growth by allocation, not wall clock (#6457)
  • ksh, zsh: a {word} before a redirection that is not a name is refused (#6592)
  • ksh, zsh: hold a placed array-literal prefix for the entries behind it (#6782)
  • ksh: OPTIND's listing and attribute corners, and PPID, TMOUT, MAILCHECK as integers (#6785)
  • ksh: a store under a missing parent is refused, and .sh takes no store (#6795)
  • ksh: a valueless subscript past the first element declares the array (#6535)
  • ksh: end an interactive -c string and script with a newline, as the prompt does (#6714)
  • ksh: export a prefix scoped to a function-form call, and keep a prefixed OPTIND as its cursor (#6596)
  • ksh: getopts and read over a typed or frozen name, and a declared OPTIND (#6634)
  • ksh: getopts's end-of-options refusal ends the script through a call, eval or . (#6688)
  • ksh: hand a caller's exported local to a function-form callee (#6745)
  • ksh: locate a failed redirection with no command word as the shell's own (#6765)
  • ksh: locate a run-time failure past an input's first line at a prompt (#6600)
  • ksh: name a here-document left open inside a substitution in a construct (#6742)
  • ksh: name an unterminated here-document over the construct it is in (#6707)
  • ksh: name line 1 of the shell's own EXIT trap body in its location (#6815)
  • ksh: read an interactive -c string and script file as a prompt reads (#6657)
  • ksh: readonly in a function-keyword function freezes only the call (#6487)
  • ksh: settle a {name} that cannot take the number before its redirection (#6646)
  • ksh: store an empty key in a table literal, refuse []= while reading (#6651)
  • ksh: trace a table literal as its elements are stored, refuse a [*]= key (#6780)
  • ksh: typeset in a POSIX-style function declares into its keyword caller (#6509)
  • ksh: warn about a table's empty key under set -x, quote a literal's traced keys (#6687)
  • ksh: write a traced word, %q and whence's name with the listing's rules (#6625)
  • zsh, bash: a {name} refused in an array literal is named as each shell names it (#6802)
  • zsh, interp: cheaper per-call option saves, fewer autoload parses, an unsorted PATH walk (#6465)
  • zsh, ksh: a prefix to a program is folded through the name's letters (#6753)
  • zsh: a builtin's whole store into a position splices, and a retyping store drops -hide (#6732)
  • zsh: a failed assignment's status at a prompt (#6580) (#6645)
  • zsh: a frozen {name} refusing a read open writes a second line (#6585)
  • zsh: a private-scope refusal through a builtin is located as the function, and leaves the store's number (#6589)
  • zsh: a refused print -v, printf -v or zparseopts store leaves 0 from -c, and zparseopts -A asks the freeze (#6555)
  • zsh: a traced assignment that fails ends its line, and a substitution writes its parent's (#6623) (#6712)
  • zsh: a {name} before &>, and >>( and <<( as a substitution (#6791)
  • zsh: an -a or -A letter in front of a literal retypes a frozen scalar, as the bare literal does (#6567)
  • zsh: an always half reads the status a failure kept (#6621, #6644) (#6669)
  • zsh: an error a -c string gave up over skips the EXIT trap (#6666) (#6825)
  • zsh: lay the default $fpath down on first use instead of at every start (#6517)
  • zsh: leave 0 where a refused getopts store ends the shell (#6700)
  • zsh: make Apply's tables once at the size they end at (#6593)
  • zsh: read a {name} across blanks to its redirection operator (#6664)
  • zsh: refuse kind letters over $0, warn on a truncated position, and keep read -A's EOF status (#6806)
  • zsh: refuse zparseopts -A over a slot whose kind is fixed (#6650)
  • zsh: standard input is located the way a prompt is (#6622) (#6755)
  • zsh: word a numeric letter over a set name's array literal as zsh does (#6670)

The interactive shell

  • zle: $PENDING counts the typed bytes behind the key, as zsh's does (#6479)
  • zle: draw PREDISPLAY before the line and honor P-flagged region_highlight (#6456)

The language and the interpreter

  • driver: stop copying the 17 KB Shell onto every frame of a start (#6586)
  • interp, syntax: trace a chained element write, and write a word back with its substitutions laid out (#6731)
  • interp: oe/o+ sort specifiers, a top-level return in qualifier code, and n in that code (#6719)
  • interp: a failed assignment keeps the old value, traced or not (#6544) (#6578)
  • interp: a failed element write keeps the element (#6579) (#6603)
  • interp: a fixed slot refuses an array prefix, and a refused set -A/read -A store leaves 0 from -c (#6516)
  • interp: a frozen name is refused where each shell refuses it, and an element literal reads its two sides in the element order (#6759)
  • interp: a frozen whole-array subscript is refused for its subscript where that refusal comes first (#6820)
  • interp: a kept reach set moves past the last call's subject, not this one's (#6819) (#6824)
  • interp: a kind letter on one half of a script's tie unties the pair (#6471)
  • interp: a listing that names an engine store has not found it (#6448)
  • interp: a pattern refused in a program's words fails only the program (#6577)
  • interp: a prefix walk stops at a refused entry before an external, and an array prefix expands its elements (#6548)
  • interp: a process substitution's body allocates from 10 under zsh's base (#6557)
  • interp: a script tie holds its kind, unties beneath a local, and a local tie starts fresh (#6484)
  • interp: a substitution's number skips nothing a fork would not hold (#6477)
  • interp: a writing substitution's body holds its own output (#6542)
  • interp: a zsh multios join holds numbers while its list applies (#6529) (#6757)
  • interp: a {name} redirection on a program is that program's own (#6519)
  • interp: a {name} redirection on a subshell is the subshell's in bash (#6549)
  • interp: a {name} store retypes the name in zsh and ksh93 (#6561)
  • interp: an array literal stops at the element that fails, and ksh keeps what it placed (#6703)
  • interp: an element write expands its value and subscript in the dialect's order (#6673)
  • interp: ask a glob qualifier's file one system call, and a list with no test none (#6455)
  • interp: ask a relative file test of the held directory, in one call (#6533)
  • interp: ask less per word and per command on a builtin's road (#6496) (#6568)
  • interp: bash saves a copy of a stream for every redirection of it (#6602)
  • interp: close every substitution end before waiting for any body (#6527)
  • interp: every pattern refusal in a program's words fails only the program (#6594)
  • interp: expand =cmd and an assignment's tilde on a copy, never in the tree (#6451)
  • interp: hold the & job running until fg continues it (#6672)
  • interp: keep a pipeline's own pipe ends out of the numbers a substitution takes (#6453)
  • interp: ksh holds a number in a command substitution's body (#6558) (#6641)
  • interp: ksh93 holds a number for a pipeline's last element (#6590)
  • interp: leave zsh's environment copy of an exported OPTARG or OPTIND where getopts found it (#6499)
  • interp: let a group skip the spans its arms' edge bytes rule out, and reuse a substitution's scratch (#6472)
  • interp: let set -a reach a name a loop, read, getopts or printf -v fills (#6474)
  • interp: let set -a reach a name an expansion or arithmetic assigns (#6463)
  • interp: name an eval parse failure by its caller under noevallineno (#6565)
  • interp: number each substitution body from a table of its own (#6676)
  • interp: order getopts's OPTARG store against its name operand per dialect (#6562)
  • interp: pin what made-first means to a writing body's table (#6556)
  • interp: read a declaration's name=$1 operand as the one parameter it is (#6373) (#6458)
  • interp: read the held directory's identity once, not on every relative path (#6423) (#6493)
  • interp: read zsh's L size qualifier, and word a qualifier subscript as math (#6792)
  • interp: refuse an unmatched pattern only after every word of the list is expanded (#6769)
  • interp: resolve every relative route from the held directory once no name reaches it (#6564)
  • interp: run a closure of one byte as a scan, and read a closure arm's spans off it (#6380) (#6492)
  • interp: run the e and + glob qualifiers' code for each file (#6689)
  • interp: take a trim's assignment off the full road and stop allocating per trim (#6460)
  • interp: take the counting loop's assignments and test off the slow roads (#6496) (#6545)
  • interp: take the started program out of the body-holds-nothing row (#6573)
  • interp: the numbers zsh holds for its own plumbing are in the numbers it publishes (#6508)
  • interp: the scope lints say nothing about the shell's own parameters (#6464)
  • interp: the scope lints' word follows the arithmetic route, not the type (#6478)
  • interp: three zsh declaration listings that wrote the wrong table (#6481)
  • interp: trace a process substitution's body one level deeper (#6800)
  • interp: what a failed literal keeps — bash's table append keeps its pairs, ksh's container letter starts the name over (#6797)
  • interp: whether a program's own {name} store is in its environment (#6570)
  • interp: write the text a zsh diagnostic quotes visibly, in every sentence (#6788)
  • interp: zsh makes a redirection list's substitutions before its saves (#6531)
  • interp: zsh's forked element holds a number from its first substitution (#6530) (#6663)
  • interp: zsh's save and restore of OPTIND around a call leaves its environment copy (#6534)
  • interp: {name} redirections — a frozen name refused first (zsh, ksh), and bash's cannot-duplicate line (#6620)
  • syntax: (z) keeps the first case arm's parenthesized pattern as one word (#6467)
  • syntax: a {name} opens no level in ksh93's ${ cmd;} body (#6643)
  • syntax: name a refused IO number as each shell does; refuse after a brace if arm one token on (#6624)
  • syntax: name what dash, ash and ksh93 name around a redirection-only shape (#6702)
  • syntax: name what zsh and ksh93 refuse behind a compound command (#6551)
  • syntax: number an open case arm at its case, and name an elif by its if, in ksh93 (#6816)
  • syntax: two places the lexer ended what zsh and ksh93 keep reading (#6729)
  • syntax: zsh reads a { where a command begins as the group, not a descriptor name (#6671)
  • syntax: zsh refuses a {name} redirection behind a compound command (#6541)

Other

  • ci: give the race leg a 30m package timeout (#6767)
  • getopts: what a cursor past the word after the last does, per dialect (#6652)
  • parse cache: decode a cached function body when it is first asked for (#6518)
  • startup: three x86-measured costs off every start, and a gate that measures the shipped build (#6495)

Full Changelog: v0.0.34...v0.0.35