Invoking Mix
mix is one binary with several entry modes: an interactive shell (REPL), a
script runner, a -c one-liner, a stdin reader, a syntax checker, a supervised
Bus daemon, and a family of one-shot meta commands (mix help, mix man,
mix stats, …). The same binary is also Mark's mesh login shell, so
ssh host '<mix source>' is evaluated by Mix, not bash. This page covers how to
start Mix in each mode and the rules that decide whether a -c / login-shell
line runs as Mix code or dispatches a shell command.
Verified against mix 0.61.0 — the binary is the oracle. Argument parsing
lives in cosmix-mix/src/main.rs;
the shell-first classifier is in cosmix-mix/src/shell.rs.
$ mix --version
mix 0.21.2
Modes at a glance
| Form | What it does |
|---|---|
mix | Interactive REPL (loads ~/.mixrc) |
mix script.mix [args…] | Run a file from the OS shell; args via args() / $1, $2, … |
mix script.mix [args…] at the REPL | Run the file in a clean child process; the REPL survives |
mix -c '<code>' | Run one line of Mix or dispatch a shell command (shell-first classifier) |
mix -i -c '<code>' | As -c, but load ~/.mixrc first (aliases + PATH), like bash -ci |
mix - [args…] | Read the whole script from stdin (clean byte channel) |
mix --check <script> | Parse without executing — a syntax check |
mix lint [flags] FILE... | Semantic analysis: undefined names, arity, must-use, stable machine-readable diagnostics (0.29.0 — see lint) |
mix --no-prelude … | Skip loading the standard prelude (applies to any run mode) |
mix --no-traceback … | Uncaught errors print the legacy single line instead of a traceback (0.29.0 — see errors) |
mix --strict-arity … | Strict call arity: wrong-arity user-function/builtin calls raise catchable ARITY_MISMATCH instead of the compatible missing→nil / extra-ignored binding (0.29.0 — see functions) |
mix --serve <script> [--name <svc>] | Run the script as a supervised Bus daemon citizen — see serve |
mix --version / -V | Print version |
mix --help / -h | Usage summary |
mix stats [sub…] | Window-labelled usage reports and static authorship coverage — see stats |
mix help / mix what <name> / mix man [topic] | Builtin/keyword reference and this manual — see the mix meta-command |
mix help, mix what, mix man, mix status, mix build, … are the same
meta commands the REPL's bareword mix dispatcher accepts, exposed as one-shot
CLI commands: mix man invocation prints this page; bare mix man prints
the manual index. In both contexts, meta names are checked
before the script-file fallback, so a script named build, status, or
test in the CWD is shadowed — prefix the path with ./, for example
mix ./build. Full list on the cli page. An unknown option
(mix --zz) errors and exits 1; an unknown REPL meta token names the token,
notes that no such file exists, then shows the meta-command overview.
Bare mix with no arguments starts the REPL — including when stdin is a
pipe. The piped lines are then read and executed one at a time through the
shell-first classifier (with ~/.mixrc loaded), so a pipe into bare mix
does run code — it works, but it is not the intended path. For a script use
mix <file> or mix -: a whole-program parse over a clean byte channel, no
per-line classification, no ~/.mixrc.
All modes that evaluate Mix record best-effort usage by default and are tagged
as interactive, script, c, stdin, or serve. Set MIX_STATS=off (also
0 or false) to skip collection and all stats I/O. See usage
statistics.
When Term supplies a native-session launch fd, Mix consumes and closes it and
scrubs COSMIX_SESSION_FD before startup hooks or user source. Binding and renews
run on a private resident thread; the seed has no Mix language or mix context
representation. Invalid bootstrap input reports the failed stage and leaves the
shell usable. Without the marker, all entry modes keep their ordinary silent
startup path. See the Term launch contract.
Running a script file
The first non-flag argument is the script path; everything after it is passed to the script.
-- greet.mix
$all = args()
print("argc=" .. length($all))
print("first=" .. $1)
for each $a in $all
print("arg: " .. $a)
end
$ mix greet.mix alpha beta
argc=2
first=alpha
arg: alpha
arg: beta
Two ways to read arguments, both populated the same way:
args()returns the list of script arguments (not counting the script path, and not counting any flag given tomixitself —mix --strict-arity greet.mix alphapasses one argument, not two). See builtins.- Positional
$1,$2, … are the same arguments by index;$0is the script path / filename. These are the one place a bare sigil number is allowed to be unbound without raising — an out-of-range positional reads asnil.
include inside a script resolves relative to the running file's directory
(not the CWD), because the runner records the script path (it is also $0, and
error diagnostics attribute to it). The every-time, CWD-relative loader is
source. See modules.
The script's exit code is 0 on success, 1 on an uncaught error (the error
message goes to stderr); a Ctrl-C interrupt is a clean exit.
A command line ending in an odd run of backslashes may continue onto the next
physical line. Mix splices it before classification, so the same command behaves
identically here, under -c, and in the REPL. Even trailing runs are literal;
backslashes inside Mix strings and heredoc bodies are untouched. See
syntax.
At the REPL or login-shell prompt, mix FILE [args…] launches the same script
mode as the OS-shell form in a child process. It therefore gets a clean
scope, $0 is FILE, and the remaining words populate args() and $1,
$2, …; when it finishes, the existing REPL resumes. Use source FILE when
the file should instead execute in and modify the current session scope.
Meta names keep precedence over files: if status exists in the CWD,
mix status still shows status and mix ./status runs the file.
A script that registers on <cmd> … end handlers does not exit after its
last statement — it stays alive pumping events for those handlers. For the
supervised daemon variant (broker registration, reconnect, journald logging),
use --serve.
mix -c — one-liners and the shell-first classifier
mix -c '<code>' runs a single line. It is shell-first: the same classifier
the REPL uses decides whether the line is Mix code or an external command. This
is what makes Mix usable as a login shell — mix -c 'hostname' dispatches a
command, mix -c 'print(1 + 1)' evaluates Mix.
$ mix -c 'print(1 + 1)'
2
$ mix -c 'echo hello from shell'
hello from shell
How the classifier decides (shell.rs classify_input)
A line starting with # or -- is a comment (empty input). Otherwise the first
word (after skipping KEY=VALUE env prefixes and expanding any alias) routes
the line:
- Starts with
$→ Mix code (e.g.$x = 1), with a shell-chain fallback. - A Mix statement-keyword head (
print,if,for,send,function,try, …) → Mix, also with the shell-chain fallback.true/false/nilare deliberately not keyword heads (since 0.21.0): a baretrueorfalseline routes to/usr/bin/{true,false}sofalseexits 1 like every shell, andnil(no external binary) falls through to the Mix parse. Inside Mix source the lexer still owns all three literals ($x = true). - A shell builtin head (
cd,pushd,popd,history,exit,which,type,jobs,mix, …) → external path. - A tight-hyphenated command-shaped head (
cosmix-comp,systemd-nspawn) → external command before Mix can parse it as subtraction, whether or not it is onPATH. The rule is statement-head-only and requires an ASCII letter/_start with only ASCII letters/digits/_/-; spaceda - b, sigil/numeric subtraction, and expression-positionprint(a-b)stay Mix. Scope does not affect the decision: bareais the string"a", not$a, soa-bremains a command when$ais live. - First word found on
PATHor is a path (./x,/bin/ls) → external command (design principle: shell-first). Routing is head-first —&&/||/;never force the shell path by themselves: a Mix-keyword head with a chain stays Mix (Mix has native statement chaining), an external head parses the chain on the shell path. - Otherwise → parse as Mix (function call, expression). If that fails definitively, a genuinely command-like head (
gti status) reports the familiar "command not found" and exits 127; a Mix-shaped head — a lexer failure (0755), an operator, a glued call/index opener (print(…) — surfaces the real lex/parse error and exits 1. An unknown command-like semicolon list (zqxfoo; zqxbar) stays on the shell path even though each bare word could parse as a discarded Mix string; this preserves the pre-0.31 shell result instead of silently succeeding. A known preserved wart is that a non-keyword alphabetic head such asnot $x; print(1)is also shell-routed and exits 127; use a Mix keyword, sigil, or parenthesized call to make the intent explicit.
Two consequences worth internalising:
$ mix -c 'false' ; echo $? # routes to /usr/bin/false — exits 1, like every shell
1
$ mix -c 'true' ; echo $?
0
$ mix -c 'print(false)' # the boolean literal, inside Mix source
false
A dispatched command's exit status becomes Mix's exit code
(mix -c '/bin/sh -c "exit 7"' exits 7). && / || / ; chaining and pipes
work on the external path, and a plain foreground cd works inside chains
(cd /tmp && pwd) — -c also has bare cd (→ $HOME), cd -, and
~-expansion. See the shell page for the full dispatch
grammar (redirects, brace expansion, $(…) substitution).
A definitive Mix lex/parse error exits non-zero with the real diagnostic — it is not silently retried as a command:
$ mix -c 'print(0755)'
Lexer error at line 1:7: ambiguous leading-zero number '0755' — use a 0o (octal) / 0x (hex) / 0b (binary) prefix, or drop the leading zero(s) for decimal
$ echo $?
1
The bare-name footgun
Every Mix variable needs a $ sigil. A bare name = value line is read as a
shell command, not an assignment:
$ mix -c 'x = 1'
mix: x: No such file or directory (os error 2)
$ echo $?
127
$ mix -c '$x = 1
print($x)' # the $ sigil makes it Mix
1
See variables for the sigil rules.
Multi-statement -c
Mix statements may be separated by ; or real newlines. Semicolons are the
compact one-line form:
$ mix -c '$x = 5; $y = 7; print($x * $y)'
35
Literal newlines remain useful for a readable multi-statement body:
$ mix -c '$x = 5
$y = 7
print($x * $y)'
35
Positional args under -c
Everything after the code string is a positional $1, $2, … — even a word
starting with - (so flags like --no-prelude must come before -c).
$0 is unset (nil) under -c.
$ mix -c 'print("got " .. $1 .. " and " .. $2)' one two
got one and two
A bare mix inside a -c line is refused (it would otherwise launch a nested
REPL):
$ mix -c 'mix'
mix: -c: bare 'mix' would start an interactive REPL — ignored
mix -i — load ~/.mixrc first
-i makes a -c run interactive-flavoured (≈ bash -ci): it sources
~/.mixrc first, so your aliases and the toolkit's PATH are in scope before
the line is classified and run. The combined spellings -ci and -ic mean the
same thing.
$ mix -i -c '<line>' # aliases expanded, ~/.mixrc PATH applied
$ mix -ci '<line>' # same
Without -i, a plain mix -c has no aliases and does not read ~/.mixrc
(≈ bash -c). ~/.mixrc also auto-loads for the plain interactive REPL
(mix with no args). It does not load for a non-interactive ssh host '<mix>' — that path is closest to mix -c.
mix - — read a script from stdin
mix - reads the entire program from stdin as a clean byte channel — no shell
re-quoting, nothing written to disk. This is the residue-free way to ship a
script to a remote host (it is also the transport under the ssh_mix builtin —
see remote).
$ echo 'print("from stdin: " .. (2 * 21))' | mix -
from stdin: 42
$ echo 'print("name=" .. $1)' | mix - worldarg
name=worldarg
Args after - are the script's positionals, exactly as with a file. $0 is set
to "-". Unlike a pipe into bare mix (which the REPL consumes line by line
through the classifier), mix - parses the whole program up front.
Running as a login shell and over SSH
When /opt/cosmix/bin/mix is root's login shell on a host, ssh host '<cmd>' is evaluated by Mix, not bash. The universal shell -c contract still
holds because the login path runs through the same shell-first classifier:
ssh host hostname— bare command, dispatched natively (no Mix needed).ssh host 'mix status'—mixself-resolves viacurrent_exe, no PATH needed for this case.ssh host '<mix source>'— send Mix source to get Mix behaviour, e.g.ssh host 'print(run("hostname"))'.
$ ssh node1 'print("hostname is: " .. run("hostname"))' # node1 runs mix as its login shell
hostname is: node1
(Locally, echo '<mix source>' | mix - runs the same script against this host.)
From a Mix script, drive a remote mix through the ssh builtins instead of raw
ssh: ssh_run($host, $cmd) for a one-liner, and ssh_mix($host, $source) —
which ships whole Mix source over ssh stdin into /opt/cosmix/bin/mix -,
bypassing every shell-quoting layer — for anything with quoting. The
ssh_mix + heredoc headline idiom
shows the interpolation and bindings rules.
Two PATH gotchas to remember on the remote side:
- Inside
run/run_rc(which spawn/bin/shwith a minimal PATH), call binaries by full path —/opt/cosmix/bin/mix, not baremix. See running commands. - A non-interactive
ssh host '<mix>'has no aliases (it's themix -cpath, notmix -i).
For Mix-as-a-shell language facts (&&/||/; chaining, pipes, redirects,
brace expansion, $(…) command substitution on a shell-dispatch line), see
the shell page.
--no-prelude
Mix loads a small standard prelude (Mix-source helpers) before running your
code; --no-prelude skips it. It applies to any run mode (-c, file, -,
--serve). Builtins are compiled in and are unaffected — only prelude-defined
helpers disappear. Give the flag before the code string / script path
(after them it would be read as a script argument); --serve also accepts it
after the script path.
$ mix --no-prelude -c 'print("" .. range(1, 3))' # range is a builtin, still works
[1, 2, 3]
The prelude source is cosmix-lib-mix/std/prelude.mix, compiled into the binary.
A user override at <base>/prelude.mix is honoured if it exists, where <base>
is $COSMIX_SRC when that variable is set, otherwise $COSMIX. (Setting
$COSMIX_SRC replaces the base — $COSMIX/prelude.mix is then not consulted.)
Otherwise the built-in prelude is used.
--check — syntax check without running
--check lexes and parses a script and prints <file>: OK, or the first
parse/lex error and exits 1. Nothing is executed.
$ mix --check good.mix
good.mix: OK
$ echo $?
0
$ mix --check bad.mix
Parse error at line 3:1: expected End, got Eof
$ echo $?
1
Use it in CI or a pre-commit hook to catch unterminated blocks (missing end),
unclosed strings, and lexer errors before deploy.
--serve — run a script as a supervised Bus daemon
mix --serve <script> [--name <svc>] runs the script as a long-lived,
supervised Bus citizen instead of a one-shot script. It connects to the local
broker (cosmix-noded), registers, and runs an unconditional event pump that
dispatches your on <cmd> … end handlers until SIGTERM/Ctrl-C or a QUIT.
$ mix --serve worker.mix # Bus service name = script stem ("worker")
$ mix --serve worker.mix --name probe # explicit service name
Name derivation (SPEC 18 §3.1): --name wins; otherwise the default is the
script's file stem. A leading cosmix- is stripped from either source
(/usr/local/lib/cosmix/statecache.mix → statecache, and --name cosmix-foo
canonicalises to foo). An anonymous serve with no derivable name is a launch
error — never a nameless citizen. Serve mode takes no positional script args
(a daemon has no argv); only --name and --no-prelude may follow the script
path, in any order.
It differs from a plain run in four ways: a reconnecting supervised transport (a
broker bounce is a transient drop, not a death); an unconditional event pump
(a plain run pumps only when the script registered handlers); an exhausted
initial connect budget that fails fast (exit non-zero) under systemd; and a
runtime-reserved verb surface — HELP / INFO / QUIT plus
<svc>.props.{get,list,describe} are answered by the runtime pre-dispatch, so
an on handler can never shadow them. Logging goes to journald (stderr
fallback off-systemd), tagged with service = <svc>. The full contract is on
the serve page; see Bus messaging for on … end handlers
and the broker model.
Resource note
Scripts run by the mix binary on the main thread get a raised recursion cap
(128 vs the library default of 16). A runaway still returns a clean error rather
than a native stack overflow. The binary does not sandbox its own operator —
time/collection caps stay unset for binary-run scripts.
See also
- variables — the
$sigil and scope rules - strings —
'raw'vs"${…}",..concat - functions —
args(), positionals,include/source - the shell page — chaining, pipes, redirects, brace/
$(…)expansion - running commands —
run/run_rcand the minimal PATH - remote execution —
ssh_run/ssh_must/ssh_mix - serve — the full
--servecitizen contract - the
mixmeta-command —mix help,mix man,mix stats, … - Bus messaging —
send/on … endand--servecitizens - builtins index —
args,range,run, … - the manual index
mix help— full categorised builtin reference;mix what <name>— per-name lookup- Mix repo · ARexx background