shell

module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: BSD-3-Clause

README

shell

Docs CI Go Reference status

A POSIX shell for WASI containers, in pure Go, built for GOOS=wasip1 GOARCH=wasm. It is what makes shell-form commands work in go-wasmcontainers: RUN a && b in a Containerfile, wctr run IMAGE sh -c '…', wctr exec -it CONTAINER sh.

A wasip1 module cannot start another program: Go's port has no fork or exec, and WASI preview 1 has no process calls. This repository uses a small subset of WASIX, in WASIX's own import module (wasix_32v1) and ABI — proc_spawn3, proc_join, fd_pipe, proc_id, proc_parent, tty_get, tty_set — and nothing else of it (no fork, no exec, no threads, no signals).

wasix //go:wasmimport bindings for the subset, with a Go API: Spawn(name, argv, *Attr), Wait(ctx, pid), Pipe(), TtyGet/TtySet. The struct encoders and decoders are portable and fuzzed, for hosts to reuse. A module of its own, github.com/go-wasmcontainers/shell/wasix, with no dependencies, so programs can import it without the shell.
wasix/exec os/exec for WASI containers: Command, Cmd (Path Args Env Dir Stdin Stdout Stderr), Start/Wait/WaitContext/Run/Output/CombinedOutput, StdinPipe/StdoutPipe/StderrPipe, ExitError, LookPath (<dir>/<name> then <dir>/<name>.wasm). A pluggable System (WASIX by default) for tests and tracing. No Kill/CommandContext: signals are not in the WASIX subset. sh itself starts its children through it.
cmd/sh sh: the mvdan.cc/sh v3.14.1 interpreter, whose external commands are started with proc_spawn3 and waited for with proc_join.
internal/run the shell's exec handler: PATH lookup (wasix/exec's rule, the pkgx bottle layout), #! scripts, plain scripts (POSIX ENOEXEC rule), children started through wasix/exec.
cmd/mkimage builds the base image (below) as an OCI image layout, and pushes it to a registry.

The base image

ghcr.io/go-wasmcontainers/shell, platform wasip1/wasm, one layer:

path
/bin/sh this shell
/bin/coreutils.wasm wasmdesk/coreutils v0.6.0, the multicall module; env CMD, xargs and find -exec start programs through wasix/exec
/bin/cat, /bin/ls, … one symbolic link to coreutils.wasm per tool (50 tools, including hostname, uname, id, whoami, clear, tee, which, xargs); the module picks the tool from argv[0]
/etc/passwd, /etc/group, /etc/os-release root only
/tmp (1777), /root

Config: Cmd ["/bin/sh"], Env PATH=/bin, WorkingDir /, and Shell ["/bin/sh","-c"], which is what a shell-form RUN, CMD or ENTRYPOINT in a Containerfile runs. The build is reproducible (every mtime is the commit time). It is published by the image workflow when a release tag is pushed (tags X.Y.Z and latest); every CI run keeps the layout as an artifact.

go run ./cmd/mkimage --sh sh.wasm --coreutils coreutils.wasm --version 0.1.0 --layout ./layout
wctr pull oci:./layout:0.1.0

It runs on a host that implements the WASIX process subset: the go-wasmcontainers engine from v0.17.0 (wctrd --wasi engine, the default), where pipelines through the coreutils work.

What sh does

  • Pipelines between builtins and programs (fd_pipe), redirections, here-documents, command substitution, cd (the child gets the directory both as a chdir fd op and as $PWD, which is where Go's wasip1 port reads it), exported variables, $?, set -e/-u/-x/-f/-a, exec, sh -c, sh file, sh -s, an interactive prompt on a TTY.
  • A line editor on a terminal (wctr exec -it c sh), the engine's own: github.com/go-wasmcontainers/engine/lineedit (a module of its own, standard library only, so sh.wasm does not link net/http), the editor wctr uses. The arrow keys, Home/End, Backspace/Delete, history (Up/Down, ^P/^N), reverse search (^R), ^A ^E ^U ^K ^W ^L, ^C to abandon a line, ^D on an empty line to end the shell with the last command's status. The terminal is in raw mode (WASIX tty_set) only while a line is typed; commands run with it in line mode. Unknown keys and escape sequences are ignored, never inserted. Wide (CJK, emoji) and combining characters are counted in columns; a line wider than the screen wraps. No Tab completion yet: the core has the hook, sh does not pass a completer.
  • Exit statuses as POSIX says: 127 not found, 126 found but cannot run, 128+n for a signal reported by the host.
  • Empty arguments and arguments with newlines reach the child intact (proc_spawn3, not the line-feed separated proc_spawn2).
  • Children inherit only fds 0–2: the shell closes its pipe ends and redirection files in the child (fd ops), so readers see EOF.

Not available, by design of the subset: signals (kill, trap on signals, Ctrl-C forwarding), job control (& runs in-process, fg, bg), process substitution <(…). exec runs the program and exits with its status rather than replacing the shell.

Build

GOOS=wasip1 GOARCH=wasm go build -o sh ./cmd/sh

It needs a host that implements the subset: the go-wasmcontainers engine (v0.17.0 and later), or wasmer (used in CI as the independent oracle). Outside such a host, starting a program fails with status 126 and says why. A redirection to /dev/null works in a container without /dev.

wasmer run --volume ./rootfs:/ --env PATH=/bin rootfs/bin/sh -- -c 'echo a | cat; exit 3'

Tests

  • natively (go test -race ./...): the ABI encoders, the fd plan (fuzzed against a simulated fd table), PATH lookup, the exec handler with a fake host, the sh command line;
  • under wasmer: the wasix package against a real WASIX host, and the shell end to end (internal/e2e);
  • in the engine itself (TestBaseImageUnderEngine): the base image's layer, sh and the real coreutils, pipelines included (a 20000-line one), which wasmer cannot run; and wctr exec c sh -c 'ls /bin | wc -l' into a running container of the image (TestExecIntoBaseImage, engine v0.20.0 and later).

Four defects of the oracle found on the way are listed in docs/REFERENCES.md, with the bibliography (WASIX header and libc shims, wasmer's implementation, mvdan.cc/sh, Go's wasip1 port, POSIX).

mvdan.cc/sh is patched

third_party/mvdan-sh is mvdan.cc/sh v3.14.1 with one change, interp.SetPipeFunc: upstream creates pipes with os.Pipe, which is ENOSYS on wasip1. See third_party/mvdan-sh/PATCHED; a test fails if go.mod and the copy disagree. Because of the replace directive, go install …@version does not work for cmd/sh; build from a clone.

License

BSD-3-Clause. third_party/mvdan-sh keeps its own BSD-3-Clause license.

Directories

Path Synopsis
cmd
mkimage command
Command mkimage builds the shell base image (wasip1/wasm): /bin/sh, the coreutils multicall module and its links, a minimal /etc.
Command mkimage builds the shell base image (wasip1/wasm): /bin/sh, the coreutils multicall module and its links, a minimal /etc.
sh command
Command sh is a POSIX shell for WASI containers.
Command sh is a POSIX shell for WASI containers.
internal
base
Package base lays out the root filesystem of the shell base image: /bin/sh, the coreutils multicall module with one symbolic link per tool, a minimal /etc, /tmp and /root.
Package base lays out the root filesystem of the shell base image: /bin/sh, the coreutils multicall module with one symbolic link per tool, a minimal /etc, /tmp and /root.
oci
Package oci builds a single-layer OCI image for wasip1/wasm, writes it as an OCI image layout and pushes it to a registry.
Package oci builds a single-layer OCI image for wasip1/wasm, writes it as an OCI image layout and pushes it to a registry.
run
Package run is the part of the shell that runs external programs: it finds them on PATH, decides how to start them (WebAssembly module, #! script or plain shell script) and hands them to a Spawner.
Package run is the part of the shell that runs external programs: it finds them on PATH, decides how to start them (WebAssembly module, #! script or plain shell script) and hands them to a Spawner.
shell
Package shell is the sh command: mvdan.cc/sh's POSIX interpreter wired to WASIX process spawning (internal/run).
Package shell is the sh command: mvdan.cc/sh's POSIX interpreter wired to WASIX process spawning (internal/run).
tools/wasip1check command
Command wasip1check builds Go packages for GOOS=wasip1 GOARCH=wasm one at a time and reports, for each, PASS or FAIL with the first error line.
Command wasip1check builds Go packages for GOOS=wasip1 GOARCH=wasm one at a time and reports, for each, PASS or FAIL with the first error line.
wasix module

Jump to

Keyboard shortcuts

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