Skip to content

@cli Builder

The @cli builder turns an annotated struct into a complete CLI without writing a parser by hand. Each field becomes an option or positional argument; defaults, types, and help text come from the code itself — never drifting from the source.

Flags and defaults

Annotate the struct with @cli(name: "...") and each field with @arg(short, long, default, help). Args.parse_args(argv) takes an array of strings and returns a typed instance:

Three calls to parse_args demonstrate: no flags (defaults), long form, and short form with =value. No real process arguments are needed — the array is injected directly, making the example runnable in the sandbox.

09-cli-flags.zolo
Playground
// Feature: `@cli` builder — declarative argument parsing

// Syntax: annotate a struct with `@cli(name: "...")`. Each field

// becomes a CLI option via `@arg(short, long, default, help)`.

// `Args.parse_args(argv)` parses an array and returns a typed instance.

// When to use: real CLI tools — instead of hand-rolling

// `process.argv()` parsing, get types, defaults, --help and --version

// for free.


@cli(name: "demo")
struct Args {
  @arg(short, long, help: "Verbose")
  verbose: bool,
  @arg(long, short: "n", default: 10, help: "Count")
  count: int,
}

// No flags — defaults kick in.

let a = Args.parse_args([])
print(a.verbose)   // expected: false

print(a.count)     // expected: 10


// Long form: --flag value

let b = Args.parse_args(["--verbose", "--count", "20"])
print(b.verbose)   // expected: true

print(b.count)     // expected: 20


// Short form, with =value syntax.

let c = Args.parse_args(["-v", "-n=42"])
print(c.verbose)   // expected: true

print(c.count)     // expected: 42

Positional arguments

@arg(positional) marks a field as positional. required makes it mandatory; multiple collects all remaining positionals into a list:

Simulates cat main.txt x y z: the first positional goes into input, the rest fill extras. Also runnable in the sandbox — no process dependency.

10-cli-positional.zolo
Playground
// Feature: `@arg(positional, ...)` — positional arguments

// Syntax: `positional` marks the field as a positional, `required`

// makes it mandatory, `multiple` collects all remaining args.

// When to use: file-input arguments (cat, mv, …), commands that

// take a target plus a variadic list of items.


@cli(name: "cat")
struct Args {
  @arg(positional, required, help: "Input file")
  input: str,
  @arg(positional, multiple, help: "Extra files")
  extras: [str],
}

// Single positional — extras stays empty.

let a = Args.parse_args(["main.txt"])
print(a.input)             // expected: main.txt

print(a.extras.len())  // expected: 0


// Multiple — first goes to `input`, the rest fill `extras`.

let b = Args.parse_args(["main.txt", "x", "y", "z"])
print(b.input)             // expected: main.txt

print(b.extras.len())  // expected: 3

print(b.extras[0])         // expected: x

print(b.extras[1])         // expected: y

print(b.extras[2])         // expected: z

Automatic --help and --version

With @cli(name, version) and help: "..." on each field, the runtime generates --help and --version without additional code. The text never goes stale because it comes directly from the declaration:

Normal parsing continues to work; to see the --help output, run zolo run 11-cli-help.zolo -- --help locally. The sandbox does not support --help via process.argv().

11-cli-help.zolo
// Feature: `--help` and `--version` are auto-generated from the struct

// Syntax: `@cli(name, version)` plus per-field `help: "..."`. Passing

// `--help` prints usage + every flag with its help text and exits;

// `--version` prints the version line.

// When to use: every real CLI. The help text reflects the struct

// declaration, so it never drifts out of sync.


@cli(name: "demo", version: "1.0")
struct Args {
    @arg(short, long, help: "Verbose output")
    verbose: bool,

    @arg(long, default: 10, help: "Number of items")
    count: int,
}

// Normal parse — defaults flow through.

let a = Args.parse_args([])
print(a.verbose)   // expected: false

print(a.count)     // expected: 10


// To see the help text, run the file with `--help`:

//   zolo run 11-cli-help.zolo -- --help

// →  Usage: demo [OPTIONS]

//    Options:

//      -v, --verbose         Verbose output

//          --count <COUNT>   Number of items (default: 10)

//      -h, --help            Print help

//      -V, --version         Print version

//

// Or `--version`:

//   zolo run 11-cli-help.zolo -- --version

// → demo 1.0

Requires the Zolo CLI/host — open in the playground or run locally.

Subcommands

Annotate an enum with @subcommand in a field of the main struct. Each variant becomes an independent subcommand (with its own flags if needed), and match handles dispatch — the same pattern as git commit / cargo build:

tool -v build --arch x86_64, tool start, and tool test — three invocations demonstrated by passing arrays directly, with no dependency on the real process.

12-cli-subcommands.zolo
Playground
// Feature: subcommands — `@subcommand` over an enum
// Syntax: declare an enum where each variant is a subcommand. A
// field with `@subcommand cmd: Command` becomes the dispatch slot.
// Each subcommand variant can declare its own flags via `@arg(...)`.
// When to use: tools shaped like `git commit`, `cargo build`, `kubectl get`.
// Variants without fields become no-flag subcommands; tuple/struct
// variants get their own flags.

enum Command {
    Build { @arg(long) arch: str },
    Start,
    Test,
}

@cli(name: "tool")
struct Args {
    @arg(short, long)
    verbose: bool,

    @subcommand
    cmd: Command,
}

// Dispatch via `match` — the idiomatic shape for an enum.
fn run(args: Args) {
    print(args.verbose)
    match args.cmd {
        .Build { arch } => print("build arch={arch}"),
        .Start => print("start"),
        .Test => print("test"),
    }
}

// `tool -v build --arch x86_64`
run(Args.parse_args(["-v", "build", "--arch", "x86_64"]))
// expected:
//   true
//   build arch=x86_64

// `tool start`
run(Args.parse_args(["start"]))
// expected:
//   false
//   start

// `tool test`
run(Args.parse_args(["test"]))
// expected:
//   false
//   test

Challenge

Add a fourth subcommand Deploy { @arg(long) env: str } to the enum and implement the corresponding arm in match. Call Args.parse_args(["deploy", "--env", "prod"]) and check the output.

enespt-br