Help us shape the future of Go! Take the 2026 Go Developer Survey

sh

module
v0.0.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 18, 2026 License: Apache-2.0

README

sh

A shell parser and interpreter in Go.

Status: early — these are 0.x tags and they mean it. The core parser and interpreter are in place, and all five dialect binaries grade against a panel of the real shells they model. Measured on macOS with make conformance-dialects, 2026-09-14, 4106 cases each:

our binary graded against exact behavioral
bash bash 5.3.15 96% 100%
zsh zsh 5.9.2 98% 99%
dash dash 98% 100%
ksh ksh93 AJM 93u+ 94% 98%
ash BusyBox 1.37 ash 80% 98%

Exact is byte-identical stdout, stderr and exit status. Behavioral lets a diagnostic be worded differently so long as the status and the output agree. The distance between the two columns is a mix of wording and behavior rather than mostly wording: the one column anybody has actually counted is ksh, re-counted on 2026-09-15, where the non-exact cases split 93 wording to 126 behavioral. Behavior lands spec-first, per CLEANROOM.md.

What makes this different

The core language is the common denominator of real shells — not strict POSIX, and not bash. Dialects (bash, zsh, ksh, dash, ash, plus core and posix) are presets over a semantics vector, not layers, translations or forks.

That distinction is not stylistic. Measuring twenty-eight behavioral axes across dash, bash, ksh93 and zsh produces eight different groupings of those shells — dash sides with zsh on echo, bash sides with zsh on shift, ksh93 sides with zsh on pipelines. No "sh → bash → zsh" ladder explains the data. Dialect is a point in a multi-dimensional space, so the model is a vector of independent switches. See docs/spec/semantics.md.

Execution is gateable and observable from the inside. Sandbox policy, agent permission prompts, AI context and audit trails are interpreter-internal seams rather than wrappers, because eval and source re-parse inside the interpreter and a wrapper never reaches them. See docs/design.md.

Independence

This is an independent implementation. It is not a fork or a port of any existing project, and no file here derives from one. Behavior is learned from POSIX, from vendor manuals, and from running real shell binaries as oracles; it is written down in docs/spec/; code is written from the spec. CLEANROOM.md is the binding rule set.

Installing

From the tap:

brew install blairham/tap/sh

Or from source:

make install                           # /usr/local/libexec/sh — needs sudo to write
make install PREFIX="$HOME/.local"     # no sudo

Six binaries: sh, and the dialect binaries bash, zsh, ksh, dash and ash. They land in libexec and not in a bin directory, because they are named after the shells they model — a directory ahead of /bin on PATH would answer for every program on the machine that resolves a shell by name. make install refuses a SHELLDIR that is on PATH unless it is told to go ahead, and the formula links nothing into Homebrew's bin for the same reason.

So you run one by its full path, and docs/install.md has the whole of it — including what /etc/shells and chsh need to make one of them a login shell, and how to try one first as a terminal profile's command, which is a checkbox to revert rather than a rescue.

A session reads everything a real shell of the same name reads, in the same order: ~/.bashrc, ~/.zshrc, the profile files, and the machine's own file in front of each. docs/install.md has the measured grid.

What is not there yet

The tags are 0.x because this list is real, not because the list is short.

  • Diagnostic wording is a long tail, not the headline gap. This list used to open by saying wording was most of the distance above, and two measurements disagree. One is the ksh re-count beside the table. The other is a bounded sample, run 2026-09-16: twenty error situations a person actually hits — a missing command, cd onto a file, set -u on an unset name, a redirect that cannot open, division by zero, an unknown option to six different builtins, running a directory, exit foo — each run once through the real shell and through ours under a matching argv[0], compared on stderr and exit status together. Byte-identical 20/20 for bash and dash, 19/20 for ksh and zsh, 16/20 for ash: 94 of 100. And of the six misses exactly one is a wording — ash says % where we say %z for a bad printf directive. The other five each carry a different exit status, a different line number, or a different answer, which is behavior rather than phrasing. Twenty common situations is a sample and not a census, and the corpus behind the percentages above is two hundred times larger and much stranger. The probe is internal/cmd/diagsample, a few seconds a column, so this bullet can be re-checked rather than inherited — which is how it came to be wrong: its only previous instrument was an hour-long conformance run, so nobody ever ran it to find out.
  • The sandbox contains the shell, not the process tree. A policy decides what the shell opens, runs and signals; a command it was allowed to start makes its own accesses and nothing here sees them. docs/design/sandboxing.md has the scope limit in full.
  • POSIX only. No Windows target — every route into this program is a process group, a controlling terminal, a signal or a syscall.Exec, and a binary that cannot start is worse than no binary.

Using it as a library

The core is a library, and it does not know which shells exist: syntax and interp define the questions, and each shell answers them in its own package under dialect/.

sem, dial, diag := bash.Semantics(), bash.Dialect(), bash.Diagnostics()
r := &interp.Runner{Semantics: &sem, Dialect: &dial, Diagnostics: &diag}
bash.Apply(r)                      // what this shell adds or removes

r.Register("cd", cd)               // only what shell cannot express

prelude, _ := syntax.Parse(`basename() { printf '%s\n' "${1##*/}"; }`, dial)
r.Run(ctx, prelude)                // everything else

Adding a shell adds a directory. Nothing under syntax/ or interp/ names one.

Functions shadow builtins and external commands alike, so most of a dialect needs no Go at all.

Layout

CLEANROOM.md              the rules that keep this independent
docs/install.md           installing, and running it as a login shell
docs/design.md            architecture
docs/lessons-carried-forward.md
                          starting constraints, learned the hard way
docs/spec/                behavioral specs — the wall
  oracle.md               how behavior is learned from real binaries
  shell-matrix.md         measured: which constructs exist where
  semantics.md            measured: where shells conflict
  core.md                 the core language boundary
  style.md                measured: how source is laid back out
  grammar/                per-construct specs
cmd/shfmt                 the formatter — every dialect the parser reads

License

Apache-2.0. See LICENSE and NOTICE.

Contributions require a signed CLA — see CONTRIBUTING.md and CLA.md. The CLA exists so the project retains the option to offer different terms later; you keep your copyright.

Directories

Path Synopsis
cmd
ash command
Command ash is a BusyBox ash dialect built on the core.
Command ash is a BusyBox ash dialect built on the core.
bash command
Command bash is a bash dialect built on the core.
Command bash is a bash dialect built on the core.
dash command
Command dash is a dash dialect built on the core.
Command dash is a dash dialect built on the core.
ksh command
Command ksh is a ksh93 dialect built on the core.
Command ksh is a ksh93 dialect built on the core.
sh command
Command sh drives the substrate.
Command sh drives the substrate.
shfmt command
Command shfmt lays out shell scripts, in every dialect the substrate parses: the core, posix, dash, bash, ksh93 and zsh.
Command shfmt lays out shell scripts, in every dialect the substrate parses: the core, posix, dash, bash, ksh93 and zsh.
zsh command
Command zsh is a zsh dialect built on the core.
Command zsh is a zsh dialect built on the core.
Package dialect holds one package per shell the substrate imitates.
Package dialect holds one package per shell the substrate imitates.
ash
Package ash answers the substrate's questions the way BusyBox ash does.
Package ash answers the substrate's questions the way BusyBox ash does.
bash
Package bash answers the substrate's questions the way bash 5 does.
Package bash answers the substrate's questions the way bash 5 does.
dash
Package dash answers the substrate's questions the way dash does.
Package dash answers the substrate's questions the way dash does.
ksh
Package ksh answers the substrate's questions the way ksh93 does.
Package ksh answers the substrate's questions the way ksh93 does.
zsh
Package zsh answers the substrate's questions the way zsh does.
Package zsh answers the substrate's questions the way zsh does.
Package driver is the front end a dialect needs in order to be a program.
Package driver is the front end a dialect needs in order to be a program.
internal
acp
Package acp speaks the Agent Client Protocol, in both directions.
Package acp speaks the Agent Client Protocol, in both directions.
acpboot
Package acpboot supplies driver.Shell.ServeACP.
Package acpboot supplies driver.Shell.ServeACP.
acpcheck
Package acpcheck is a real ACP client, used to grade a real ACP agent.
Package acpcheck is a real ACP client, used to grade a real ACP agent.
axismutate
Package axismutate moves one field of a settings struct to a value it does not hold, so that something else can ask whether anything noticed.
Package axismutate moves one field of a settings struct to a value it does not hold, so that something else can ask whether anything noticed.
axissweep
Package axissweep asks, of every axis in interp.Semantics, whether anything fails when it is moved.
Package axissweep asks, of every axis in interp.Semantics, whether anything fails when it is moved.
blocks
Package blocks stores a command and what came of it as one record.
Package blocks stores a command and what came of it as one record.
boundary
Package boundary asks a shell's gate about what a *front end* does itself.
Package boundary asks a shell's gate about what a *front end* does itself.
charset
Package charset writes a code point as the byte a single-byte locale charset stands it in.
Package charset writes a code point as the byte a single-byte locale charset stands it in.
charsetgen command
Command charsetgen turns Unicode's character-mapping tables into the single-byte charsets a locale can name.
Command charsetgen turns Unicode's character-mapping tables into the single-byte charsets a locale can name.
childguard
Package childguard fails a test binary that leaves a process behind whose command line names a marker the caller chooses.
Package childguard fails a test binary that leaves a process behind whose command line names a marker the caller chooses.
cmd/acpcheck command
Command acpcheck drives the shell's Agent Client Protocol front end the way an editor would — as a subprocess, over JSON-RPC on a pipe — and prints what works, what it cost, and what an ordinary pipe would have seen instead.
Command acpcheck drives the shell's Agent Client Protocol front end the way an editor would — as a subprocess, over JSON-RPC on a pipe — and prints what works, what it cost, and what an ordinary pipe would have seen instead.
cmd/axissweep command
Command axissweep moves every axis in interp.Semantics and reports the ones nothing objected to.
Command axissweep moves every axis in interp.Semantics and reports the ones nothing objected to.
cmd/corpusguard command
Command corpusguard fails when the oracle corpus has lost a case.
Command corpusguard fails when the oracle corpus has lost a case.
cmd/coverage command
Command coverage reports, per dialect, every element of the shell's surface that no case in the tree mentions.
Command coverage reports, per dialect, every element of the shell's surface that no case in the tree mentions.
cmd/diagsample command
Command diagsample runs one bounded sample of error situations through a real shell and through ours, and reports how many came back byte-identical.
Command diagsample runs one bounded sample of error situations through a real shell and through ours, and reports how many came back byte-identical.
cmd/fmtwild command
Command wild formats every shell script it can find and holds each result to the formatter's three promises: the tree survives, the comments survive, and a second pass changes nothing.
Command wild formats every shell script it can find and holds each result to the formatter's three promises: the tree survives, the comments survive, and a second pass changes nothing.
cmd/histprobe command
Command histprobe drives a shell through a pseudo-terminal with a TWO-ROW prompt and types lines at it one at a time, waiting on a distinct done-marker after each.
Command histprobe drives a shell through a pseudo-terminal with a TWO-ROW prompt and types lines at it one at a time, waiting on a distinct done-marker after each.
cmd/oracle command
Command oracle records what real shells do, and fails when that changes.
Command oracle records what real shells do, and fails when that changes.
cmd/oraclerunner command
Command oraclerunner is the oracle harness, inside a container.
Command oraclerunner is the oracle harness, inside a container.
cmd/sandboxcheck command
Command sandboxcheck grades the boundary from outside the process: it runs the shell that ships, tries every way a script has of reaching the filesystem, and asks the filesystem what happened.
Command sandboxcheck grades the boundary from outside the process: it runs the shell that ships, tries every way a script has of reaching the filesystem, and asks the filesystem what happened.
cmd/smoke command
Command smoke drives a realistic interactive session through a real terminal, for each dialect binary it is given, and prints what worked.
Command smoke drives a realistic interactive session through a real terminal, for each dialect binary it is given, and prints what worked.
cmd/suitecheck command
Command suitecheck fetches a shell's own test suite and runs every file of it through both that shell and the dialect binary claiming to be it.
Command suitecheck fetches a shell's own test suite and runs every file of it through both that shell and the dialect binary claiming to be it.
cmd/suiteguard command
Command suiteguard fails when our own conformance suite has lost a file or shrunk one.
Command suiteguard fails when our own conformance suite has lost a file or shrunk one.
cmd/suiteinside command
Command suiteinside is the half of `make suite` that runs inside a container image, for the one column whose reference shell exists nowhere else.
Command suiteinside is the half of `make suite` that runs inside a container image, for the one column whose reference shell exists nowhere else.
cmd/tworowprobe command
Command tworowprobe drives a dialect binary through a pseudo-terminal with a two-row prompt and reports what reached the screen.
Command tworowprobe drives a dialect binary through a pseudo-terminal with a two-row prompt and reports what reached the screen.
cmd/viprobe command
Command viprobe types one key sequence into one shell through a pseudo-terminal and prints the line that came back.
Command viprobe types one key sequence into one shell through a pseudo-terminal and prints the line that came back.
cmd/wild command
Command wild parses the shell scripts installed on this machine and reports the ones this parser cannot read.
Command wild parses the shell scripts installed on this machine and reports the ones this parser cannot read.
corpusguard
Package corpusguard checks that the oracle corpus never loses a case.
Package corpusguard checks that the oracle corpus never loses a case.
coverage
Package coverage asks what a body of shell cases never mentions.
Package coverage asks what a body of shell cases never mentions.
depsurface
Package depsurface has no code.
Package depsurface has no code.
dialecttest
Package dialecttest builds the interp.Runner a dialect package's own tests run their snippets on.
Package dialecttest builds the interp.Runner a dialect package's own tests run their snippets on.
eastasian
Package eastasian answers whether a code point is East Asian Width W or F, which is the one thing two readers of it have to agree about.
Package eastasian answers whether a code point is East Asian Width W or F, which is the one thing two readers of it have to agree about.
event
Package event writes an interpreter's event stream down in a stable, front-end-neutral form.
Package event writes an interpreter's event stream down in a stable, front-end-neutral form.
fdset
Package fdset asks the kernel which descriptors a read would not wait on.
Package fdset asks the kernel which descriptors a read would not wait on.
fmt/comments
Package comments recovers the comments the substrate's parser discards.
Package comments recovers the comments the substrate's parser discards.
fmt/diff
Package diff renders a unified diff between two texts, for the formatter's -d mode.
Package diff renders a unified diff between two texts, for the formatter's -d mode.
fmt/printer
Package printer lays out a parsed script one way.
Package printer lays out a parsed script one way.
fmt/walk
Package walk visits every node of a github.com/blairham/sh syntax tree.
Package walk visits every node of a github.com/blairham/sh syntax tree.
histexpand
Package histexpand rewrites a line the way a shell's history expander does: `!!` becomes the previous command, `!$` its last word, `^old^new^` the previous command with one word changed.
Package histexpand rewrites a line the way a shell's history expander does: `!!` becomes the previous command, `!$` its last word, `^old^new^` the previous command with one word changed.
normgen command
Command normgen turns Unicode's UnicodeData.txt into the tables internal/unorm decomposes with, and NormalizationTest.txt into the fixture that proves it agrees with Unicode.
Command normgen turns Unicode's UnicodeData.txt into the tables internal/unorm decomposes with, and NormalizationTest.txt into the fixture that proves it agrees with Unicode.
opened
Package opened answers the one question a name-based gate cannot answer for itself: what path did this open actually go through?
Package opened answers the one question a name-based gate cannot answer for itself: what path did this open actually go through?
oracle
Package oracle records what real shells do.
Package oracle records what real shells do.
panicguard
Package panicguard turns a panic raised while running a script into an ordinary diagnostic.
Package panicguard turns a panic raised while running a script into an ordinary diagnostic.
plugin
Package plugin runs a builtin whose implementation is in another process, in whatever language its author chose.
Package plugin runs a builtin whose implementation is in another process, in whatever language its author chose.
policy
Package policy is a declarative interp.Gate: a set of rules over paths and action kinds, and a default for everything they do not mention.
Package policy is a declarative interp.Gate: a set of rules over paths and action kinds, and a default for everything they do not mention.
pty
Package pty opens a pseudo-terminal pair, so that a test can hand a shell a real terminal.
Package pty opens a pseudo-terminal pair, so that a test can hand a shell a real terminal.
sandboxcheck
Package sandboxcheck grades the boundary from outside the process, by running the binary that ships and asking the filesystem what happened.
Package sandboxcheck grades the boundary from outside the process, by running the binary that ships and asking the filesystem what happened.
secret
Package secret finds credentials in text.
Package secret finds credentials in text.
smoke
Package smoke drives a realistic interactive session through a real terminal and reports, feature by feature, what worked.
Package smoke drives a realistic interactive session through a real terminal and reports, feature by feature, what worked.
startupcost
Package startupcost measures how long a shell takes to become useful.
Package startupcost measures how long a shell takes to become useful.
suite
Package suite runs a real shell's own test suite through both that shell and the dialect binary that claims to be it, and reports where they part.
Package suite runs a real shell's own test suite through both that shell and the dialect binary that claims to be it, and reports where they part.
suiteguard
Package suiteguard checks that our own conformance suite never loses a file or shrinks one.
Package suiteguard checks that our own conformance suite never loses a file or shrinks one.
terminfofixture
Package terminfofixture builds compiled terminal descriptions for tests.
Package terminfofixture builds compiled terminal descriptions for tests.
testenv
Package testenv gives a test suite that starts shells an environment of its own, assembled from nothing rather than inherited from whoever ran `go test`.
Package testenv gives a test suite that starts shells an environment of its own, assembled from nothing rather than inherited from whoever ran `go test`.
treeguard
Package treeguard fails a test binary that leaves a file behind in the directory it ran in.
Package treeguard fails a test binary that leaves a file behind in the directory it ran in.
tty
Package tty answers one question: is this open file a terminal.
Package tty answers one question: is this open file a terminal.
unorm
Package unorm answers whether two strings are the same text, which is not the same question as whether they are the same bytes.
Package unorm answers whether two strings are the same text, which is not the same question as whether they are the same bytes.
widthgen command
Command widthgen turns Unicode's EastAsianWidth.txt into the table the line editor measures with.
Command widthgen turns Unicode's EastAsianWidth.txt into the table the line editor measures with.
wild
Package wild sweeps the shell scripts installed on the machine and reports which of them this parser cannot read.
Package wild sweeps the shell scripts installed on the machine and reports which of them this parser cannot read.
Package interp executes a syntax tree.
Package interp executes a syntax tree.
Package repl is the interactive surface: a prompt, a line editor, and the loop between them.
Package repl is the interactive surface: a prompt, a line editor, and the loop between them.
Package syntax turns shell source into tokens and a syntax tree.
Package syntax turns shell source into tokens and a syntax tree.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL