std/cli

A program with several commands, flags, options and positional arguments, declared once as a builder - the help text and the errors are generated from it, and parsing runs over a list of strings, so a test needs no process.

Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite

Die generierte Referenz von std/cli zeigt jede öffentliche Deklaration mit Signatur, Doc-Kommentar und Beispielen.

std/cli is the standard library's answer to Rust's clap: a program with several commands, flags, options and positional arguments, its help text and its errors, declared once. The language has no annotations and no reflection, so the declaration is a builder on receiver closures, the same means project.trb uses. Nothing is registered by scanning: a subcommand is part of the tree because a command block names it.

The package is pure except for Command.run, which reads the arguments it is given and exits the process on a wrong line. Command.parse and Command.help take a List<String> and answer a value, so a test hands them argument lists and compares what comes back (see std/test). The arguments of the running program are Process.arguments() of std/process.

torb check, build, run, test, manifest, ir, natives, parse, tokens, ast and highlight read their own command lines with this package (Command lines): what torb test --help prints is the help text it generates, and a wrong line is answered with its errors.

Import#

use command, Command, Parsed, Input, Argument, CliError from "std/cli"
use command from "std/cli"
use Process from "std/process"

fn main() {
  const cli = command "greet" {
    description "Greets people"
    flag "shout", help: "Write the greeting in capitals"
    option<Int>("times", help: "How often", default: 1)
    arguments "names", help: "Who to greet"
    run { input =>
      for name in input.arguments("names") {
        const greeting = "Hello, {name}"
        for _ in 0..input.option<Int>("times") {
          print if input.flag("shout") { greeting.toUpperCase() } else { greeting }
        }
      }
    }
  }
  cli.run Process.arguments()
}

The declaration#

A command block has the members of CommandBuilder: description for the one line of the help, flag, option<T> and arguments for what the command reads, command for a subcommand, which is a block of the same members, and run for what happens with the line that was read. A command with subcommands needs no run; a line that names none of them is then an error that lists them.

  • flag("verbose") is --verbose, and input.flag("verbose") is true where the line has it and false where it does not. A flag takes no value; --verbose=yes is an error.
  • option<T>("jobs", default: 4) is --jobs 8 or --jobs=8, and input.option<Int>("jobs") is the value, or the default where the line has none. An option without a default is required.
  • arguments("paths") collects every argument that is not a flag or an option, in order, as a list: input.arguments("paths"). -- ends the options, so what follows it is an argument even when it starts with -.
  • -h and --help print the help of the command the line chose, and help followed by the names of a command prints that one. Every command has both without declaring them.

Reading a name the command did not declare, or an option as a type other than the one it was declared as, is a bug of the program and panics, the way an index out of range does.

Typed options#

Options are typed: the type argument of option<T> decides how the text of the line is read, and Int, Float64, Bool and String do. A type of the program joins them by implementing Argument, which says how it is written in the help text and how a word of the line becomes a value or a reason why not:

use command from "std/cli"
use Argument from "std/cli"

type Level with Show {
  case Low
  case High
}

extend Level with Argument {
  static fn argumentName(): String {
    "level"
  }

  static fn fromArgument(text: String): Result<Self, String> {
    match text {
      "low" => Ok Level.Low
      "high" => Ok Level.High
      _ => Fail "neither low nor high"
    }
  }
}

fn tool() {
  command "tool" {
    option<Level>("level", help: "How much to say", default: Level.Low)
    run { input => print input.option<Level>("level") }
  }
}

Optional, repeatable and pass-through#

Three more shapes cover what a toolchain's commands say:

  • optional<T>(name, help:) is an option that may be left out and has no default. The handler reads it with input.optional<T>("name"), a T? that is None where the line left it out; the help row says (optional).
  • repeatable<T>(name, help:) is an option the line may give any number of times, --filter a --filter=b. The handler reads input.repeated<T>("name"), a List<T> in the order of the line, empty where the line left it out. Every value is checked against T like the value of an option; the help row says (repeatable).
  • arguments(name, help:, passthrough: true) makes the first positional argument end option parsing: it and everything behind it, flags included, go into the list unread. --help and -h are honoured in front of it only, and -- in front of it works as usual. The help row says (pass-through).
use command from "std/cli"

fn tool() {
  command "torb" {
    command "test" {
      optional<String>("target", help: "Only this target")
      repeatable<String>("filter", help: "Only tests whose name has this")
      run { input =>
        print "{input.optional<String>("target")} {input.repeated<String>("filter")}"
      }
    }
    command "run" {
      arguments "program", help: "The program and its own arguments", passthrough: true
      run { input => print input.arguments("program") }
    }
  }
}

With these, torb test --filter a --filter b reads ["a", "b"], and torb run tool.trb --emit-c hands --emit-c on to the program.

Decoding the options of a command into a type of the program through Decode, as std/json does, is not part of this package yet.

The help text#

The help of a command is generated from its declaration: the description, the usage line, the subcommands, the arguments, the flags and the options with their defaults, each section with its own column. For a check command of a torb program:

Check projects, files or a whole workspace

Usage: torb check [options] [paths...]

Arguments:
  paths  Projects, directories or files

Flags:
  -h, --help    Show this help text
  --statistics  Print typed and deferred expressions per module

Options:
  --jobs <integer>  How many files at once (default: 4)
  --ratio <number>  (default: 0.5)
  --color <bool>    Colored output (default: true)
  --profile <text>  The profile to use (default: dev)

The errors#

A wrong line is a CliError, and its message names the command and the argument. Command.run prints it, then a hint to --help, on standard error and exits with code 2; Command.parse answers it as the Fail of a Result, and show() is the message:

MistakeMessage
unknown subcommandtorb: unknown command 'chek' (known: check, build)
a command that needs a subcommandtorb remote: a command is required (known: add, list)
unknown flag or optiontorb check: unknown flag '--verbos'
an option at the end of the linetorb check: option '--jobs' needs a value
a value of the wrong typetorb check: option '--jobs': 'many' is not an integer
a value given to a flagtorb check: flag '--statistics' takes no value
a required option that is missingtorb build: option '--output' is required
an argument the command does not taketorb version: unexpected argument 'now'

Testing a command line#

parse reads the line without running anything, so the declaration of a program and what it reads are tested with argument lists:

test "jobs defaults to four and reads both spellings" {
  const parsed = cli.parse(["check", "--jobs=8"]).unwrap()
  assert(parsed.input.option<Int>("jobs") == 8)
}

Declarations#

The command function#

fn command(name: String, configure: (var self: CommandBuilder) => Void): Command

The function a program starts with: command "name" { ... } builds the tree, and run reads a line against it.

Command#

type Command {
  name: String
  summary: String
  flags: List<FlagSpec>
  options: List<OptionSpec>
  positional: ArgumentsSpec?
  commands: List<Command>
  handler: ((Input) => Void)?
  fn subcommand(name: String): Command?
  fn parse(arguments: List<String>): Result<Parsed, CliError>
  fn help(path: List<String>): String
  fn run(arguments: List<String>)
  fn names(): List<String>
}

The declared command with its subcommands. parse answers what a line reads as, or the CliError of the first mistake in it; a line with --help or help is a Parsed with helpRequested. help(path) is the help text of the command the names lead to. run is the entry of a program: it prints the help or runs the handler, and a wrong line ends the process with code 2.

CommandBuilder#

type CommandBuilder {
  name: String
  protected var summary: String = ""
  protected var flagSpecs: List<FlagSpec> = []
  protected var optionSpecs: List<OptionSpec> = []
  protected var argumentsSpec: ArgumentsSpec? = None
  protected var subcommands: List<Command> = []
  protected var action: ((Input) => Void)? = None
  var fn description(text: String)
  var fn command(name: String, configure: (var self: CommandBuilder) => Void)
  var fn flag(name: String, help: String = "")
  var fn option<T: Argument>(name: String, help: String = "", default: T? = None)
  var fn optional<T: Argument>(name: String, help: String = "")
  var fn repeatable<T: Argument>(name: String, help: String = "")
  var fn arguments(name: String, help: String = "", passthrough: Bool = false)
  var fn run(handler: (Input) => Void)
}

What a command block declares. option<T> with a default shows it in the help; without one, the option is required. optional<T> and repeatable<T> are the options that may be left out, and arguments with passthrough hands everything from the first positional argument on to the handler unread.

Parsed#

type Parsed {
  path: List<String>
  command: Command
  input: Input
  helpRequested: Bool
  fn help(): String
}

What a line read as: the path of the command it chose (["torb", "check"]), that command, its input, and helpRequested where the line asked for the help instead of for the work.

Input#

type Input {
  path: List<String>
  flagValues: Map<String, Bool>
  optionValues: Map<String, String>
  argumentValues: Map<String, List<String>>
  optionalValues: Map<String, List<String>>
  repeatedValues: Map<String, List<String>>
  fn flag(name: String): Bool
  fn option<T: Argument>(name: String): T
  fn optional<T: Argument>(name: String): T?
  fn repeated<T: Argument>(name: String): List<T>
  fn arguments(name: String): List<String>
}

The values of a line, as the handler of run reads them.

Argument#

trait Argument with Show {
  static fn argumentName(): String
  static fn fromArgument(text: String): Result<Self, String>
}

The types an option can have. argumentName is how the help text calls the value (<integer>), fromArgument reads a word, and its Fail is the reason that follows the word in the message ('many' is not an integer).

CliError#

type CliError with Show, Error {
  case UnknownCommand(command: String, name: String, known: List<String>)
  case MissingCommand(command: String, known: List<String>)
  case UnknownFlag(command: String, flag: String)
  case MissingValue(command: String, option: String)
  case InvalidValue(command: String, option: String, value: String, reason: String)
  case ValueForFlag(command: String, flag: String)
  case MissingOption(command: String, option: String)
  case UnexpectedArgument(command: String, argument: String)
  fn command(): String
  fn show(): String
}

A wrong line, with the command it was for in command() and the message in show().

FlagSpec, OptionSpec and ArgumentsSpec#

type FlagSpec {
  name: String
  help: String
}
type OptionSpec {
  name: String
  help: String
  typeName: String
  defaultText: String?
  check: (String) => String?
  optional: Bool
  repeatable: Bool
}
type ArgumentsSpec {
  name: String
  help: String
  passthrough: Bool
}

What a Command keeps of each declaration, for a program that lists its own commands.

Esc