cli

tape
import cli from "cli";
import io from "io";
import os from "os";

pub fn main() -> i32 {
    let spec: cli.Spec = cli.Spec {
        name: "grep";
        about: "Print lines that contain a pattern.";
        opts: [
            cli.Opt { long: "ignore-case"; short: "i"; help: "Match upper and lower case alike"; },
            cli.Opt { long: "max"; short: "m"; help: "Stop after this many lines"; takes_value: true; },
        ];
        positionals: ["pattern"];
        rest: "files";
    };
    let args: []const []const u8 = os.arg_list();
    let p: cli.Parsed = cli.parse(spec, args) or {
        if (err == cli.CliError.HelpRequested) { io.print(cli.usage(spec)); return 0; }
        io.eprintln("grep: {cli.message(spec, args)}");
        return 2;
    };
    let pattern: []const u8 = p.positional(0)!;
    let max: []const u8 = p.value_or("max", "100");
    let ignore_case: bool = p.flag("ignore-case");
    // ... search the files named by p.positional(1), p.positional(2), ...
    return 0;
}

You describe the options and arguments your program takes; cli reads a command line against that description and you ask the result for what was given. cli is pure: it prints nothing and never exits, so what happens on --help or a mistake is up to you.

The spec

tape
pub struct Opt {
    long: []const u8;          // "max" for --max
    short: []const u8 = "";    // "m" for -m, or "" for none
    help: []const u8 = "";     // one line for the usage text
    takes_value: bool = false; // --max 5, --max=5, -m 5, -m5
}

pub struct Spec {
    name: []const u8;                       // the program, for the usage text
    about: []const u8 = "";                 // a sentence under the usage line
    opts: []const Opt = [];
    positionals: []const []const u8 = [];   // required arguments, in order
    rest: []const u8 = "";                  // name of any further arguments, or ""
}

Leave out the fields you don’t need: every field but long and name declares an empty default. Write the arrays right in the literal when you build the spec in the function that parses (usually main): those arrays live in that function, so don’t return a spec or a Parsed built that way. To share a spec, put its options in a const table, which lives as long as the program, and return a spec built from it:

tape
const OPTS: [2]cli.Opt = [
    cli.Opt { long: "jobs"; short: "j"; help: "Run this many at once"; takes_value: true; },
    cli.Opt { long: "dry-run"; short: "n"; help: "Only print"; },
];

fn spec() -> cli.Spec {
    return cli.Spec { name: "make"; opts: OPTS[0..]; rest: "targets"; };
}

A mistake in the spec itself (an empty or duplicate name, a short longer than one letter or digit, an option called help) stops the program with a trap: it’s a bug in the program, not something its user did.

Parsing

FunctionReturns
parse(spec, args) -> Parsed or CliErrorThe parsed command line; args[0] (the program) is skipped

What parse understands:

  • --max 5 and --max=5; -m 5 and -m5 for an option that takes a value. A value is taken as it is, even - or -5.
  • Grouped short flags: -iv is -i -v; -im5 is -i -m 5.
  • -- ends the options: everything after it is an argument. A lone - is an argument too.
  • Options anywhere: before, between or after the arguments, and as often as you like.
  • --help, and -h unless one of your options uses h, give HelpRequested, even when the line has other mistakes. Print cli.usage(spec) then.

Errors:

CliErrorWhen
UnknownOptionAn option not in the spec
MissingValueAn option that takes a value is last
UnexpectedValue--flag=x for an option without a value
UnexpectedArgumentMore arguments than positionals and no rest
MissingArgumentFewer arguments than positionals names
HelpRequested--help or -h

cli.message(spec, args) says what is wrong in words, for your error report: unknown option '--frob', option '--max' needs a value, missing argument <pattern>.

Asking for what was given

MethodReturns
flag(long) -> boolWhether --long was given
count(long) -> i64How many times (-vvv is 3)
value(long) -> ?[]const u8Its value (the last one), or null
value_or(long, default) -> []const u8Its value, or default
values(long) -> ValueIterEvery value, in order: for (dir in p.values("include"))
positional(i) -> ?[]const u8Argument i (the named ones, then the rest), or null
positional_count() -> i64How many arguments there are

Options are named by their long name without dashes. Asking for a name the spec doesn’t have traps, so a typo shows up the first time the code runs. Values are text: turn them into numbers with conv:

tape
let n: i64 = conv.parse_i64(p.value_or("max", "100")) or {
    io.eprintln("grep: --max: {err}");
    return 2;
};

Nothing allocates: values and arguments are views of args.

Usage text

cli.usage(spec) returns the help text:

plaintext
Usage: grep [options] <pattern> [<files>...]

Print lines that contain a pattern.

Options:
  -i, --ignore-case  Match upper and lower case alike
  -m, --max <max>    Stop after this many lines
  -h, --help         Print this help

Subcommands

cli has no subcommands of its own. Look at the first argument yourself and parse the rest with that command’s spec. parse skips the first element of what you pass, so args[1..] puts the command in the program’s place:

tape
let args: []const []const u8 = os.arg_list();
if (args.len > 1 && bytes.eq(args[1], "commit")) {
    let p: cli.Parsed = cli.parse(commit_spec, args[1..]) or { ... };
}

Last modified: