Skip to main content

Building a CLI tool

Minimal

fn main() -> Result<(), Error> {
let args = env.args();
match args.get(1).map(|s| s.as_str()) {
Maybe.Some("--help") | Maybe.Some("-h") => print_help(),
Maybe.Some(name) => greet(&name),
Maybe.None => print_help(),
}
Result.Ok(())
}

fn greet(name: &Text) {
print(&f"Hello, {name}!");
}

fn print_help() {
println(&"usage: greet <name>");
}

With subcommands

type Command is
| Build { release: Bool, target: Maybe<Text> }
| Test { filter: Maybe<Text> }
| Version;

fn parse_args(args: &List<Text>) -> Result<Command, Error> {
match args.get(1).map(|s| s.as_str()) {
Maybe.Some("build") => {
let mut release = false;
let mut target = Maybe.None;
for a in args.iter().skip(2) {
match a.as_str() {
"--release" => release = true,
s if s.starts_with("--target=")
=> target = Maybe.Some(s[9..].to_string()),
_ => return Result.Err(Error.new(&f"unknown flag {a}")),
}
}
Result.Ok(Command.Build { release, target })
}
Maybe.Some("test") => {
let filter = args.get(2).cloned();
Result.Ok(Command.Test { filter })
}
Maybe.Some("version") => Result.Ok(Command.Version),
Maybe.Some(c) => Result.Err(Error.new(&f"unknown command: {c}")),
Maybe.None => Result.Err(Error.new(&"no command given")),
}
}

fn main() {
match parse_args(&env.args()).and_then(dispatch) {
Result.Ok(()) => (),
Result.Err(e) => { eprintln(&f"error: {e}"); env.exit(1); }
}
}

Reading config files

fn load_config() -> Result<Config, Error> {
let home = env.home_dir().unwrap_or(Text.from("."));
let path = match env.var_opt(&Text.from("MYTOOL_CONFIG")) {
Some(p) => p,
None => f"{home}/.mytool/config.toml",
};

match fs.read_to_string(&Path.from(&path)) {
Result.Ok(text) => toml_parse(&text).map_err(Error.from),
Result.Err(e) if e.kind == IoErrorKind.NotFound => Result.Ok(Config.default()),
Result.Err(e) => Result.Err(Error.from(e)),
}
}

Notes:

  • Verum has no format! macro — string interpolation uses the f"..." literal.
  • Maybe<T> (the Option-equivalent) deconstructs through match, not Rust-style .unwrap_or_else(closure). Use .unwrap_or(default) for an eager default value.

Coloured error output

mount term.style.{Color, Style};

fn report_error(e: &Error) {
let red = Style.new().fg(Color.Red).add_modifier(Modifier.Bold);
eprint(&red.paint(&"error: "));
eprintln(&e.to_string());
for src in e.chain().skip(1) {
eprintln(&f" caused by: {src}");
}
}

Exit codes

fn main() {
match run() {
Result.Ok(()) => env.exit_success(),
Result.Err(e) if e.is_user_error() => { report_error(&e); env.exit(1); }
Result.Err(e) if e.is_config_error() => { report_error(&e); env.exit(78); } // sysexits EX_CONFIG
Result.Err(e) => { report_error(&e); env.exit(1); }
}
}

Progress bars & spinners

mount term.widget.{Spinner, SpinnerFrames};

async fn slow_task() {
let mut spin = Spinner.new().frames(&SpinnerFrames.Dots);
spin.start(&"loading");
let result = heavy_compute().await;
spin.stop(&f"done in {result.elapsed:?}");
}

See also