Non-interactive invocations and shell setup code run in POSIX sh
Canonical Quirl project documentation synced from docs/decisions/2026-10-04_044214653_non-interactive-invocations-and-shell-setup-code-run-in-posi.md.
Accepted · 2026-10-04
Decision
When another program starts Quirl as a shell, Quirl behaves exactly like
/bin/sh, because that program wrote POSIX shell code:
quirl -c 'code' [name [argument...]]replaces itself with/bin/sh -c 'code' [name [argument...]].-land--loginpass through as-l;-iis accepted and ignored when a command is given. Clusters such as-lcand-ilcare recognized.- A script on standard input (
quirlwith a non-terminal stdin, or-s) runs as/bin/sh -s. This replaces the earlier behavior of reading Lua from standard input;quirl run --lang lua -keeps that capability explicit. - An interactive login session (argv0 beginning with
-, orquirl -lat a terminal) first captures the environment/bin/sh -lexports, under a 5-second deadline and the session byte limit, then re-executes Quirl with it. Failure keeps the inherited environment and reports why.
These options are parsed before Clap and only when the argument vector begins with them, so Quirl's own command-line interface is unchanged.
In an interactive or quirl exec session, shell setup code also runs in
/bin/sh: eval word... and source file [arg...] (or . file) start one
foreground /bin/sh pipeline whose EXIT trap reports the working directory
and exported environment. Quirl then adopts both. Session locals are passed in
as single-quoted assignments; variables set without export, functions, and
aliases stay inside the island.
A Normal-mode line that uses a compound or expansion form the native grammar
does not implement (for, while, if, case, { ...; }, [[, ((, or
brace expansion such as {a,b}) runs as an implicit eval island: the whole
line is single-quoted and handed to the same /bin/sh mechanism, so pasted
Bash and Zsh loops work and still change the session's exports and directory.
This refines the explicit-island rule of the 1.0 scope decision for
interactive Normal mode; bash { ... } and zsh { ... } remain the way to
choose a dialect explicitly. Function definitions are rejected with an
explanation instead, because a function cannot outlive its island.
Quirl's native grammar gains the POSIX variable forms people type daily: a
bare NAME=value sets an unexported shell variable (or updates an exported
one), NAME=value command sets a variable for one command (run through
/usr/bin/env, so every spawn path applies it identically), export NAME
exports an existing variable, and unset NAME removes one. Only an unquoted
NAME= prefix makes an assignment, as in sh.
Context
Quirl advertised Bash muscle memory, yet FOO=1 cmd, A=1, eval, source,
., and unset all failed, and -c did not exist. That breaks far more than
typing habits:
sshdruns remote commands as$SHELL -c '...', sossh host cmd,scp,rsync, and Git over SSH fail against a host whose login shell is Quirl.- Editors resolve a login environment with
$SHELL -l -i -c ...; coding agents and build tools run$SHELL -c. - Common setup idioms (
eval "$(ssh-agent -s)",eval "$(brew shellenv)",source .venv/bin/activate,source .env) are POSIX code generated forsh-family shells. Their absence is a frequently cited reason people return from Nushell to Zsh.
Alternatives considered:
- Interpret
-cwith Quirl's native grammar, falling back toshfor unsupported syntax. Rejected: native execution is verified only for the C1 subset, and a caller cannot know which engine ran its code. Machines need one exact contract. - Emulate
evalandsourcenatively. Rejected for the same reason: setup scripts use functions, conditionals, and dialect details; partial emulation would fail unpredictably. A realshplus an explicit import boundary is exact for everything the session can represent. - Import variables set without
export. Rejected:shgives no portable, unambiguous way to list them, and children would not see them in Bash or Zsh either.
Consequences
- Quirl is safe as a login shell for SSH, file transfer, Git, editors, and
agents, with
shexit statuses and streams preserved byte for byte. evalandsourcecost one/bin/shstart (milliseconds) and cannot define functions or aliases in Quirl; their catalog entries say so. A virtual environment'sdeactivateis therefore unavailable.- Piping Lua into a bare
quirlno longer works; usequirl run --lang lua -. - Pasted loops and conditionals run without retyping, but in
/bin/sh: Debian'sdashlacks Bash-only syntax such as[[, which still needs an explicitbash { ... }there. Shell functions are not available interactively. - Windows has no
/bin/sh:-c, standard-input scripts,eval, andsourcereport that a POSIX shell is required. - Login environments come from
/etc/profileand~/.profile, not Zsh's startup files. Users moving from Zsh should keep login-wide variables in~/.profile.
Complete like Zsh and ask Zsh for arguments
Canonical Quirl project documentation synced from docs/decisions/2026-09-30_002231168_complete-like-zsh-and-ask-zsh-for-arguments.md.
Rust 1.99 compatibility and selective adoption
Canonical Quirl project documentation synced from docs/decisions/2026-10-04_162535286_rust-1-99-compatibility-and-selective-adoption.md.