CLI Tools

On this page 7

Command-line tools are judged on two things: whether they start instantly and whether they are one file to install. A native binary with no runtime gets both for free.

Why Home suits this

One file to ship. home build produces a native executable. Nothing to install beside it, no interpreter version to match, no node_modules to resolve at start-up.

Start-up you can measure in microseconds. There is no VM to warm and no collector to initialise. For a tool invoked in a loop from a shell script, that difference dominates everything else.

Errors as values. A CLI spends most of its code on things that can fail: missing files, bad flags, malformed input. Each of them returns a Result, so the failure path is written explicitly rather than caught somewhere up the stack.

Parsing arguments

enum Command {
  Build(string),
  Check(string),
  Version,
}

fn parse_args(args: []string): Result<Command, UsageError> {
  match args {
    ["build", path] => Ok(Command.Build(path)),
    ["check", path] => Ok(Command.Check(path)),
    ["--version"] | ["-v"] => Ok(Command.Version),
    _ => Err(UsageError.Unknown),
  }
}

fn main() {
  match parse_args(env.args()) {
    Ok(Command.Build(path)) => build(path),
    Ok(Command.Check(path)) => check(path),
    Ok(Command.Version) => print("1.0.0"),
    Err(e) => {
      print("usage: mytool [build|check] <path>")
      exit(1)
    },
  }
}

Slice patterns handle the argument shapes directly, and the compiler will not let a new Command variant go unhandled. See pattern matching.

Reading files and streaming

import std::fs

fn count_lines(path: string): Result<int, Error> {
  let contents = fs.read_to_string(path)?
  Ok(contents.lines().count())
}

? returns early on a missing file, and the caller decides what the user sees. See the standard library for the file, process and formatting APIs.

Exit codes and output

Exit status is part of a tool's contract, so it is worth being deliberate about it: a Result at the top of main maps cleanly onto zero and non-zero, and diagnostics go to stderr while data goes to stdout.

Home's own tools

Home's toolchain is the best worked example. home fix, home symbols, home explain, home size and home api-diff are all built this way and are described in DX commands. If you want to see how a non-trivial CLI is structured, that is the code to read.

Status

The interpreter runs CLI programs today. Native single-entrypoint builds work through LLVM on arm64 and x86-64 macOS and Linux; bundling an imported module graph into one binary is still in progress, so a multi-file tool is not yet a single-file distribution. See the capability matrix.

Released under the MIT License.