Tools to create command-line applications.
This package makes it easier to create powerful command-line applications in Toit.
It provides:
- Composable subcommands:
myapp subcommand
- Type options/flags that parse arguments:
myapp --int-flag=49 enum_rest_arg
- Automatic help generation
- Command aliases
- Functionality to cache data between runs
- Functionality to store configurations
- A UI class to support output at different verbosity levels and different output formats
The Command
class is the main class of this package. Even programs without sub-commands
contain at least one root-command.
A command declares
- the parameters it takes (options and rest)
- help
- examples
- a lambda to execute if the command is called
For example:
import cli show *
main args/List:
command := Command "my-app"
--help="My app does something."
--options=[
Option "some-option"
--help="This is an option."
--required,
Flag "some-flag"
--short-name="f"
--help="This is a flag.",
]
--rest=[
Option "rest-arg"
--help="This is a rest argument."
--multi,
]
--examples=[
Example "Do something with the flag:"
--arguments="--some-option=foo --no-some-flag rest1 rest1",
]
--run=:: | invocation/Invocation |
print invocation["some-option"]
print invocation["some-flag"]
print invocation["rest-arg"] // A list.
invocation.cli.ui.result "Computed result"
command.run args
In this example, the command is called "my-app". It takes an option some-option
and a
flag some-flag
. It also takes multiple rest arguments rest-arg
.
The name of the root command is used to compute the location of the cache and config
paths. For example, if the command is called my-app
, the cache will be stored in
~/.cache/my-app
and the config will be stored in ~/.config/my-app/config
. See
the cache/config section, for more details.
Commands can have subcommands. For example, the git
command has subcommands like
git commit
and git push
.
Typically, subcommands are defined if the options/rest arguments of different actions
are different. For example, git commit
takes a message, but git push
does not.
Subcommands are defined by adding a Command
object as child to another command object.
For example:
import cli show *
main args/List:
command := Command "my-app"
--help="My app does something."
sub := Command "subcommand"
--help="This is a subcommand."
--run=:: | invocation/Invocation |
print "This is a subcommand."
command.add sub
command.run args
Options are parameters that are passed to the command. This package comes with several classes to simplify validation and conversion of common types.
Typed options are options that take a value of a specific type. For example, an option that takes an integer value.
Here is an incomplete list of the available typed options. See the documentation of the library for a complete list.
cli.OptionInt
: An option that takes an integer value. If the value is not an integer, an error is returned.cli.OptionString
: An option that takes a string value. Thecli.Option
constructor is an alias for this class.cli.Flag
: An option that takes a boolean value. Flags are treated specially, in that they can be negated with ano-
prefix. For example, if a flag is calledsome-flag
, it can be negated with--no-some-flag
.cli.OptionEnum
: An option that takes a value from a set of allowed values.
Users are encouraged to extend the cli.Option
class and create their own typed options.
A call to command.run
parses the given arguments and then executes the
appropriate lambda. The lambda receives one argument: an Invocation
object.
The Invocation
object contains:
cli
: ACli
object that contains common functionality for CLI applications, like thecache
,config
, andui
objects. It is common to pass this object to functions that are called from the lambda.parameters
: An object that contains the parsed options and rest arguments. TheInvocation
object has a shortcut operator[]
that forwards to theparameters
.path
: A list of strings that contains the path to the command that was called.command
: The command that was called.
The cache is a simple key-value store that persists between runs. Cached data may
be removed at any point without major implications to the user. It is typically
stored in ~/.cache/<command-name>
. Environment variables, such as $XDG_CACHE_HOME
, or
$APP_CACHE_DIR
(where APP
is the capitalized name) can be used to change the
location of the cache. See the documentation of the cache
library for more details.
The cache can either store bytes, or handle paths to cached folders.
The cache can store bytes. For example:
import cli show Cli FileStore
store-bytes cli/Cli:
cache := cli.cache
data := cache.get "my-key": | store/FileStore |
// Block that is called when the key is not found.
// The returned data is stored in the cache.
print "Data is not cached. Computing it."
store.save #[0x01, 0x02, 0x03]
print data // Prints #[0x01, 0x02, 0x03].
The FileStore
class provides convenience methods to store data. For example, it
allows to store (either copy or move) existing files:
import cli show Cli FileStore
import host.file
store-from-file cli/Cli:
cache := cli.cache
data := cache.get "my-file-key": | store/FileStore |
// Block that is called when the key is not found.
print "Data is not cached. Computing it."
store.with-tmp-directory: | tmp-dir |
data-path := "$tmp-dir/data.txt"
// Create a file with some data.
file.write-content --path=data-path "Hello world"
store.move data-path
print data // Prints the binary representation of "Hello world".
When caching multiple files, it's more convenient to just get access to a
directory in the cache structure. The cache class has the
get-directory-path
method for this use case:
import cli show Cli DirectoryStore
import host.file
store-directory cli/Cli:
cache := cli.cache
directory := cache.get-directory-path "my-dir-key": | store/DirectoryStore |
// Block that is called when the key is not found.
// The returned directory is stored in the cache.
print "Directory is not cached. Computing it."
store.with-tmp-directory: | tmp-dir |
// Create a few files with some data.
file.write-content --path="$tmp-dir/data1.txt" "Hello world"
file.write-content --path="$tmp-dir/data2.txt" "Bonjour monde"
store.move tmp-dir
print directory // Prints the path to the directory.
The config is a simple key-value store that persists between runs. It is typically
stored in ~/.config/<command-name>/config
. Environment variables, such as $XDG_CONFIG_HOME
, or
$APP_CONFIG_DIR
(where APP
is the capitalized name) can be used to change the
location of the config. See the documentation of the config
library for more details.
The Config
class behaves very similar to a Map
object. The keys must be
strings, and the values can be any json-serializable object.
When modifying a configuration it is necessary to write
the changes back to disk.
import cli show Cli Config
config-example cli/Cli:
config := cli.config
print "old value: $(config.get "my-key")"
config["my-key"] = "my-value"
config.write
Keys are split at "." to allow for nested values. For example:
dotted-example cli/Cli:
config := cli.config
print "old value: $(config.get "super-key.sub-key")"
config["super-key.sub-key"] = "my-value"
config.write
In the config file, super-key
is implemented as a map that contains a key sub-key
.
After running the two examples, the config file will contain (edited for readability):
{
"my-key": "my-value",
"super-key": {
"sub-key": "my-value"
}
}
The UI class provides a way to output information to the user. It supports different verbosity levels and different output formats.
Unless the run
method is called with a UI
object, the CLI parser will automatically
add the following options to the root command:
--output-format text|json Specify the format used when printing to the console. (default: text)
--verbose Enable verbose output. Shorthand for --verbosity-level=verbose.
--verbosity-level debug|info|verbose|quiet|silent Specify the verbosity level. (default: info)
A corresponding UI object is then available in the Cli
object. Whenever the
program wants to output something, it should use the ui
object.
import cli show Cli
some-chatty-method cli/Cli:
ui := cli.ui
ui.debug "This is a debug message."
ui.verbose "This is a verbose message."
ui.inform "This is an information message."
ui.warn "This is a warning message."
ui.error "This is an error message."
ui.interactive "This is an interactive message."
ui.result "This is a result message."
Depending on the verbosity-level some of these messages will be ignored. If the verbosity level is:
debug
: all messages are printedverbose
: all messages exceptdebug
are printedinfo
: all messages exceptdebug
andverbose
are printedquiet
: onlyinteractive
,error
andresult
messages are printedsilent
: onlyresult
messages are printed
The "result" message is special, in that it is always printed. There should only be one result message per command (if it makes sense).
The output-format allows the user to change the format of the output. At the moment 'text' and 'json' are supported. The default is 'text'. When the output-format is 'json', then all non-result messages are printed on stderr, and the result message is printed as a structured object on stdout.
Developers are encouraged to use the ui.emit --structured
method to emit structured
data. This is especially true for the result message.
import cli show *
main args:
cmd := Command "my-app"
--help="My app does something."
--run=:: run it
run invocation/Invocation:
ui := invocation.cli.ui
ui.emit
// Block that is invoked if structured data is needed.
--structured=: {
"result": "Computed result"
}
// Block that is invoked if text data is needed.
--text=: "Computed result as text message."
The Ui
class has furthermore convenience methods to print tables, maps and lists:
emit-table
: Prints a table.emit-map
: Prints a map.emit-list
: Prints a list.
Typically, these methods are used for result messages, but they can be used for other messages as well.
The shorthands ui.info
, ui.debug
, also dispatch to these methods if they receive a
table (list of lists), map or list.
See the documentation of the ui
library for more details.
Please file feature requests and bugs at the issue tracker.