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.
The generated reference of std/cli has every public construct with its signature, its doc comment and its examples.
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, andinput.flag("verbose")istruewhere the line has it andfalsewhere it does not. A flag takes no value;--verbose=yesis an error.option<T>("jobs", default: 4)is--jobs 8or--jobs=8, andinput.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-.-hand--helpprint the help of the command the line chose, andhelpfollowed 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 withinput.optional<T>("name"), aT?that isNonewhere 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 readsinput.repeated<T>("name"), aList<T>in the order of the line, empty where the line left it out. Every value is checked againstTlike 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.--helpand-hare 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:
| Mistake | Message |
|---|---|
| unknown subcommand | torb: unknown command 'chek' (known: check, build) |
| a command that needs a subcommand | torb remote: a command is required (known: add, list) |
| unknown flag or option | torb check: unknown flag '--verbos' |
| an option at the end of the line | torb check: option '--jobs' needs a value |
| a value of the wrong type | torb check: option '--jobs': 'many' is not an integer |
| a value given to a flag | torb check: flag '--statistics' takes no value |
| a required option that is missing | torb build: option '--output' is required |
| an argument the command does not take | torb 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): CommandThe 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.
Related#
- std/process -
Process.arguments(), the line a program reads, andProcess.exit. - std/test - the tests that hand
parseargument lists. - Command lines - the design, and what comes after this package.
- The standard library - the other packages.