SCUA

How-to

Read command-line arguments

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 value fills any field. --name=value works too.
  • A bool is a presence flag: --verbose sets it. --verbose=false turns it off, for a command line something else generated.
  • -x is the field whose name starts with x, 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.txt binds file. Bools are skipped, since a presence flag has no bare spelling.
  • A field with a default is optional; one without it is required.
  • where is 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.txt is rejected as the wrong type rather than quietly binding the single word — and an enum field can't be filled from either. Take a string and 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)