Building a CLI tool
Minimal
env needs its mount, and the failure names something elsecore.base.env declares args() -> List<Text>, and all three
spellings compile today:
mount | call site |
|---|---|
mount core.base.env; | env.args() |
mount core.base.env.{args}; | args() |
| (none needed) | core.base.env.args() |
Omit the mount and the bare env does not fail as an unknown name.
It resolves to core/meta/contexts.vr's fn env(key: Text) -> Maybe<Text>, which the prelude makes visible, so the error is
no method named 'args' found for type 'fn(Text) -> Maybe<Text>' —
a type error about a different env entirely. An earlier version of
this note read that as name resolution being broken for the module. It
is not; the mount was missing.
mount core.base.env;
fn main() {
let argv = env.args();
match argv.get(1) {
Maybe.Some(first) => {
if first == "--help" || first == "-h" {
print_help();
} else {
greet(&first);
}
}
Maybe.None => print_help(),
}
}
fn greet(name: &Text) {
print(f"Hello, {name}!");
}
fn print_help() {
print("usage: greet <name>");
}
With subcommands
mount core.base.env;
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 thef"..."literal. Maybe<T>(theOption-equivalent) deconstructs throughmatch, not Rust-style.unwrap_or_else(closure). Use.unwrap_or(default)for an eager default value.
Coloured error output
Style does not render itselfStyle describes an attribute set for the terminal RENDERER
(core.term.render.buffer.set_style paints it into a cell buffer). It
has no paint, no to_ansi, and nothing that turns a Style plus a
Text into an escape-wrapped string — checked against its 25 methods.
Writing colour straight to a stream means emitting the escape yourself,
which is what core/cli/help.vr does.
const RED_BOLD: Text = "\x1b[1;31m";
const RESET: Text = "\x1b[0m";
fn report_error(e: &Error) {
eprint(&f"{RED_BOLD}error: {RESET}");
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
- base → env — CLI args, env vars, paths.
- io → process — running external commands.
- term — colours, spinners, full TUI.