When you run a script, anything you type after the file name is passed to the program:
scua roll.scua 3 --hard
You read those arguments with the sys module:
import sys
let args = sys.args()
sys.args() returns an array of strings
— the user arguments only, 0-based. It does not include the
interpreter or the script path itself (so args[0] is your
first real argument, not the file name — no off-by-one). It is always an
array, empty when there are none, so you never need a nil check.
sys is a plain, always-available module like
math or str. It needs no capability
grant — arguments are inert, public, fixed-at-launch data
(they're right there on the command line), not the secret-bearing
ambient authority that environment variables are. (Environment
variables, when they arrive, will be a granted capability for
exactly that reason.)
#The grammar
scua [scua-options] <file.scua> [program-arguments...]
SCUA's own options (--fast, --allow-fs,
-D, …) come before the file. Once the file
name is seen, everything after it belongs to your program,
verbatim — including tokens that look like flags:
scua --fast game.scua level3 --hard --players=2
# └ scua's └ file └──────── all yours ─────────┘
So inside game.scua, sys.args() is
["level3", "--hard", "--players=2"]. If you ever need to
pass an argument that would otherwise be read as a scua option, put a
bare -- before the file to stop scua's own option parsing
early:
scua -- ./--oddly-named.scua arg1
--help and --version are yours
too. scua answers those for itself only when they appear among
its options — before the script path. After the file they are
ordinary arguments, so a tool of your own can implement them:
scua tidy.scua --help # your script sees ["--help"] and prints your usage
scua --help # scua's own usage
#A declared argument record
Past three or four flags, hand-parsing stops being worth it. Declare
a record instead and let the compiler do the rest —
including a --help you don't write:
import sys
record Args {
file: string,
out: string = "-",
verbose: bool = false,
count: int where count in 1:101 = 1,
}
let args = sys.parse_args(Args, { usage = "tidy <file> [-o OUT] [--verbose] [--count N]" })
if args.verbose then print(`tidying {args.file} -> {args.out}, {args.count} pass(es)`) end
Each field becomes an option, and the rules are short enough to hold in your head:
--name valuefills any field.--name=valueworks too.- A bool is a presence flag:
--verbosesets it.--verbose=falseturns it off, for a command line something else generated. -xis the field whose name starts withx, when exactly one does. Two fields starting with the same letter simply get no short flag between them; the long forms always work.- Positional arguments fill the fields nothing named,
in declaration order — so
tidy in.txtbindsfile. Bools are skipped, since a presence flag has no bare spelling. - A field with a default is optional; one without it is required.
whereis checked exactly as it is anywhere else.- The declared type is checked, and that includes
shapes an argument can't be spelled as. A command line supplies text, so
a collection field (
files: { string }) can only be filled from a JSON body on stdin —--files a.txtis rejected as the wrong type rather than quietly binding the single word — and an enum field can't be filled from either. Take astringand convert it yourself when the value comes from a command line.
Anything wrong — an unknown option, a missing required field, a value
of the wrong type, a failed where, one argument too many —
prints a structured error and exits 2, without running
your code. The error names the field, what arrived and what was
declared:
$ tidy a.txt
{"error":{"message":"field 'files' of Args is string, expected { string }","field":"files","record":"Args","value":"a.txt"}}
#The generated --help
--help (and -h, unless a field starts with
h) prints usage built from the declaration and exits 0:
$ tidy --help
usage: tidy <file> [-o OUT] [--verbose] [--count N]
--file, -f VALUE string (required)
--out, -o VALUE string default -
--verbose, -v bool default false
--count, -c N int default 1
The usage: line is the usage option when
you give one, and generated from the record when you don't. There's no
generated --version — a version string isn't something the
record knows, so print your own.
#Reading a JSON body instead
The same call also binds a JSON object piped on stdin, which is how an AI agent invokes a tool. Values from stdin win, and a field it supplied is not re-filled from a positional. See Write tools for AI agents.
#Parsing arguments
Arguments are just strings — parse them however your program needs,
using the value-producing if, membership with
in, and tonumber for numerics:
import sys
let args = sys.args()
let name = if args.len() > 0 then args[0] else "world" end -- positional, with a default
let loud = "--loud" in args -- a presence flag
let count = if args.len() > 1 then tonumber(args[1]) else nil end
let times = if count == nil then 1 else count end -- numeric option with a fallback
print(`hello, {name}`)
for _ in 0:times do
print(if loud then "TICK" else "tick" end)
end
$ scua greet.scua alice 2 --loud
hello, alice
TICK
TICK
See the runnable examples/args.scua.
#Agent tools
For stdin / JSON result / exit-status tools (the
shape AI agents and shell harnesses want) — sys.parse_args,
sys.emit / sys.fail, scua schema,
and scua pack — see Write
tools for AI agents.
#Scope: the entry program only
sys.args() (and sys.stdin()) return data
for the program you launched. Inside a spawned actor, both are empty
([] / "") — an actor is an isolated partition
and doesn't inherit the launching program's arguments or stdin (the same
isolation that keeps capabilities from leaking across partitions). Pass
an actor whatever it needs explicitly, in the message that starts
it.
#A note on encoding
A SCUA string is always valid UTF-8. If an argument
isn't valid UTF-8 (rare — usually a stray byte on POSIX, or a lone
surrogate on Windows), the launch fails immediately with a clear message
rather than silently dropping the argument and shifting every later
index:
$ scua tool.scua $'\xff'
scua: tool.scua: argument #0 is not valid UTF-8 (a SCUA string must be UTF-8)