CLI reference
The primary ash CLI is a machine interface. Its successful output and error
diagnostics use stable ASON, with no human help page, color, or progress bar.
The separate, feature-gated ash shell route is explicitly human-facing and
uses conventional bytes, diagnostics, and exit statuses instead of ASON.
ash self replace is an internal Windows helper used by activation, not a normal
Harness interface.
Human shell checkpoints
The current human-shell checkpoints open a cross-platform line-edited REPL when stdin is
a terminal and executes the current command and pipeline syntax sequentially
against one persistent native state. It implements pwd, echo (with -n),
cd, expanded export/unset, set pipefail control, exit, portable ls,
raw-byte cat, text grep, portable cp, mv, rm, create-only touch,
named and last-status parameter expansion, nested command substitution,
quote-aware pathname expansion, native host executables, explicit
WSL commands on Windows, plus same-line native/WSL/portable/stateful
pipelines, left-associative &&/|| conditional lists, and ordered native,
WSL, portable, and stateful redirections. Source
may also come from -c SOURCE, a native
script-file path, or stdin:
File, stdin, and Profile sources must be valid UTF-8 and cannot exceed 1 MiB.
Native file paths are not converted to UTF-8; use
ash shell -- ./-script.ash for an
operand beginning with -. Portable ls accepts one path (default .), emits
one stably ordered native name per line, and supports -a/--all,
-d/--directory, -1, combined short options, and --. Portable cat
requires one path, writes exact bytes without an added newline, accepts --,
and shares the 128 MiB read/capture ceiling. In a multi-stage pipeline, cat -
consumes the incoming byte stream; in a simple command it consumes an explicit
< file. An unredirected standalone -, options, and multiple files remain
errors. Portable grep requires one valid-UTF-8 regular file and
uses Rust regular expressions by default. It
supports -E/--extended-regexp, -F/--fixed-strings,
-i/--ignore-case, -n/--line-number, combined short options, and --.
Searches are capped at 64 MiB; no matches return status 1 without a diagnostic.
In a multi-stage pipeline, grep PATTERN - consumes incoming UTF-8 with the
same semantics and a 128 MiB output ceiling; a simple command may provide -
through <. Directories, multiple files, unredirected standalone stdin -, and
unsupported options fail explicitly.
Portable cp SOURCE DESTINATION, mv SOURCE DESTINATION, rm PATH, and
touch PATH accept only those exact regular-file arities plus --. The
persistent cwd is their durable transaction root. Parent traversal, escape,
symlink/reparse traversal, directories, files above 128 MiB, and paths that
cannot be represented in the UTF-8 journal fail explicitly. Copy, move, and
touch never overwrite; touch creates a new empty file rather than updating an
existing timestamp. Copy, move, and remove derive a BLAKE3 source preimage
immediately before the shared ASH/1 fs transaction revalidates it. A conflict
returns status 1 without retry and preserves the external change or destination.
The root's reserved .ash directory holds locking, rollback, and restart
recovery state.
Startup Profiles are opt-in. --profile FILE selects one explicitly, a
non-empty ASH_PROFILE supplies the configured default, and --no-profile
disables both. Relative paths are anchored to the initial cwd. A Profile is
parsed completely before any command runs: malformed startup never applies a
prefix. A non-interactive invocation stops with status 2; an interactive shell
prints the same source-spanned diagnostic and opens in safe mode. Profile state,
including cd and exported variables, persists into the selected input.
The prompt defaults to ash> ; ASH_PROMPT replaces it and must be valid
UTF-8. Ctrl+C at the prompt sets $? to 130 and continues, while EOF returns
the previous status. exit [STATUS] stops the remaining submitted source and
the REPL. An omitted status reuses $?; explicit values must be integers from
0 through 255, and invalid arguments return 2 without exiting.
ASH_HISTORY selects persistent history relative to the initial cwd, and an
empty value disables it. Defaults are $XDG_STATE_HOME/ash/history then
$HOME/.local/state/ash/history on Unix-like hosts, and
%LOCALAPPDATA%\ash\history on Windows. Leading-space or leading-tab lines are
not recorded. Symbolic links and non-regular targets are rejected, Unix history
files are forced to mode 0600, and an unsafe or unavailable path produces a
warning while the in-memory editing session remains usable.
export NAME=VALUE and unset NAME persistently update both shell-variable and
exported environment state. Each accepts one expanded argument and --; names
must be ASCII shell identifiers, empty values are preserved, and unsetting a
missing name succeeds. Quote values that can contain separators, as in
export COPY="$SOURCE". Listing and multiple names are not implemented yet.
set -o pipefail persistently enables rightmost-failure pipeline status;
set +o pipefail restores the default final-stage policy. Any other set form
returns status 2. A startup Profile can select the policy. In a pipeline, set
uses that stage's state clone and cannot mutate the parent policy.
$NAME, ${NAME}, and $? expand immediately before each command resolves.
Shell variables precede host-aware environment lookup and undefined names are
empty. Single quotes and escaped dollars remain literal. Double quotes preserve
one native field, including an empty field; unquoted values split on fixed ASCII
space, tab, and LF separators, and an unquoted empty expansion can remove its
word. IFS does not configure splitting in this checkpoint. Native string units
remain lossless through expansion and direct argv launch.
Nested $(SOURCE) command substitutions are parsed into the same typed script
plan, with exact outer-source diagnostics and a 32-level depth limit. They run
in source order in command words and file-redirection targets. Each receives a
full ShellState clone, so cwd, variables, environment, options, status, and
exit remain local; ordinary external process and filesystem effects remain
visible, including when a later pipeline stage fails outer preflight. Ash
captures stdout, removes every trailing LF, preserves one field and
internal newlines inside double quotes, and applies the fixed ASCII split only
when unquoted. A nonzero nested status does not replace parent $? or block the
outer command; stderr and diagnostics propagate once. NUL output is rejected.
Unix preserves non-UTF-8 bytes, while Windows requires valid UTF-8. Substitution
values, stdout, and stderr share the remaining 128 MiB synchronous allowance;
a capture failure prevents the outer command from running. Short-circuited
pipelines execute no substitutions, and full-source parsing still precedes all
effects.
After fixed unquoted field splitting, pathname expansion recognizes active *,
?, [abc], ascending [a-z], and negated [!abc] or [^abc] operators in
command fields and redirection targets. Single quotes, double quotes, and a
backslash protect operators; unquoted parameter and command-substitution output
can introduce them. Relative patterns enumerate from persistent cwd, absolute
patterns stay absolute, and results sort by lossless native path units with
case-sensitive matching. A wildcard component selects a leading-dot name only
when it begins with a literal dot. ** is not recursive. Malformed classes and
patterns with no matches fail before launch; redirections still require exactly
one match. Each command and its redirections share limits of 32,768 active
pattern units, 65,536 inspected directory entries, and 4,096 matches. Skipped
conditional branches perform no directory scan.
Native programs resolve through the persistent PATH or an explicit native:
prefix. The resolved host executable receives the parsed argument vector,
current directory, and exported environment directly; ash never inserts a host
shell command string. An unredirected standalone child receives null stdin and synchronously
captured output, so programs that require their own foreground terminal remain
part of H4 job control rather than this line-edited REPL. Stdout and stderr share
the remaining 128 MiB capture allowance, and a normal nonzero native exit
becomes the command status without an added diagnostic.
On Windows, linux:COMMAND explicitly selects WSL. Ash first resolves
wsl.exe; a missing launcher returns status 126 with a resolution diagnostic,
and an unresolved ordinary command never falls through to WSL. The wrapper
receives an optional distribution, the current Windows cwd through --cd, then
--exec, the Linux command, and each argument as a separate native string. No
host or Linux shell string is inserted. The CLI currently uses the user's
default distribution; an embedding may select one through ShellOptions, and
the selected WSL backend/distribution is retained in command status.
Native, WSL, and portable commands plus implemented stateful builtins accept
<, >, >>, 2>, 2>>, 2>&1, and 1>&2.
Assignments apply from left to right: command >out 2>&1 merges both streams
into out, while command 2>&1 >out leaves stderr on the original stdout
capture. Every file target, including $(...) and pathname patterns, must
finish as exactly one native field; relative
paths resolve against persistent cwd. Files connect directly to child or
parent-task OS handles, so redirected output does not consume the synchronous
capture allowance. One validated graph order interleaves native, portable, and
stateful files by stage and redirection source order; superseded targets are still
opened. Missing, ambiguous, or unopenable targets return status 1 with a
redirection diagnostic. Source-spanned shell diagnostics remain shell stderr,
not raw command stderr. Stateful and portable-mutation arguments and targets preflight before opens;
simple-command files open before parent mutation, and failed opens block the
state mutation, portable filesystem transaction, or exit request. WSL redirection files are opened by the Windows host
and connect directly to the wrapper's standard handles.
A same-line | forms a pipeline of two to 32 stages. Each may be a native host
command, explicit WSL command on Windows, portable pwd, echo, ls, cat,
grep, cp, mv, rm, or touch, or implemented stateful cd, export,
unset, set, or exit;
aliases, functions, and unimplemented stateful commands fail during complete
preflight. Native and WSL wrapper pairs remain connected by direct OS pipes.
Boundaries involving an in-process stage
keep only explicit asynchronous parent ends and poll the bounded task
concurrently with native execution and capture, preserving OS backpressure without
materializing native or portable cat/grep streams in full. Only cat - and
grep PATTERN - consume incoming stdin; other portable forms close that reader,
so an upstream producer can observe broken-pipe failure. Final in-process output,
final native output, and captured native stderr share the remaining 128 MiB
allowance. Status defaults to the final stage. With set -o pipefail, the
rightmost unsuccessful native, WSL, portable, or stateful stage is selected,
including 128 + signal; an all-success pipeline still uses its final stage.
Stateful stages never consume stdin, execute against independent ShellState
clones, and close their empty stdout on completion. The parent cwd, variables,
environment, options, and exit request remain unchanged; downstream readers see
EOF, and pipeline exit contributes only an ordinary stage status.
Mutation stages likewise close stdin, emit no stdout, and contribute their
transaction conflict or filesystem status to final-stage/pipefail selection.
Their redirection files open in graph order before the transaction begins.
Any native, WSL, portable, or implemented stateful stage may redirect stdin,
stdout, or stderr, with descriptor duplication still applied in source order. If a producer no longer references
an internal pipe, ash closes its parent writer and the downstream reader sees
EOF. If a consumer replaces its pipe stdin, ash closes the parent reader and the
upstream writer sees native broken-pipe behavior. Final-stage or pipefail
selection then determines the visible status. A descriptor copy that still
references the pipe remains connected even if the original descriptor is later
redirected. Stateful files participate in the same global open order; these
builtins emit no raw command output, and redirecting their empty stdout closes
an outgoing pipe normally. A WSL stage reuses the same validated pipe and file
graph without a relay buffer.
After parent pipe/file handles are claimed, ash converts the native graph into
one job supervisor. Native exits retain specification order, and any setup,
capture, or wait failure terminates and reaps every native member's owned
process tree before the pipeline returns. Portable/stateful futures and capture
drains settle before the supervisor enters a non-cancelled ordered wait under
that same completion boundary.
&& and || link complete pipelines with equal precedence and evaluate left
to right. && admits the next pipeline only after status 0; || admits it only
after a nonzero status. The input is the visible final-stage or pipefail
pipeline status. A skipped branch leaves that status unchanged for the next
link and $?, emits no diagnostic, and performs no expansion, resolution,
argument preflight, redirection open, process launch, state change, or portable
filesystem transaction. Only an admitted exit stops the source. The complete
submitted source still parses before effects, so malformed skipped syntax blocks
every prefix command. Layout, comments, and newlines may continue a source after
an operator; an unlinked newline or ; starts a new unconditional list.
The remaining command language (including aliases, functions, and
subshell state), terminal
streaming, foreground interactive programs and jobs, plus installed
WSL distribution probing, backend policy/configuration, general argument path
mapping, environment forwarding, Linux-side ownership, and interruption
normalization are not implemented yet. The regular binary enables the
human-shell
feature;
cargo build -p a3s-ash --no-default-features keeps a minimal machine-only
build.
Error contract
Example:
For machine commands, exit codes map stably to error classes. Callers should
branch on ASON diagnostics and exit status, never localized text. Missing
arguments and unknown machine commands return a usage-class machine error
instead of printing help. ash shell deliberately emits human diagnostics and
conventional command statuses.
Working directory
File operations are confined to the startup workspace. Paths must use canonical
logical form and cannot escape through lexical traversal, symbolic links, or
Windows reparse points. exec never switches to an implicit host shell.
Human-shell state instead uses native PathBuf and OsString values. cd,
export, unset, ls, cat, and grep inherit normal user authority.
Portable cp, mv, rm, and touch deliberately bind the current cwd as a
journaled workspace and cannot traverse above it. Native external programs
inherit the same cwd/environment state and ordinary OS authority. Native path
and environment units are preserved or reversibly escaped; this does not
weaken or replace ASH/1 workspace confinement.