diff --git a/README.md b/README.md index 11c1cfc..d46e24b 100644 --- a/README.md +++ b/README.md @@ -27,33 +27,33 @@ A command declares For example: ``` toit -import cli +import cli show * main args/List: - command := cli.Command "my-app" + command := Command "my-app" --help="My app does something." --options=[ - cli.Option "some-option" + Option "some-option" --help="This is an option." --required, - cli.Flag "some-flag" + Flag "some-flag" --short-name="f" --help="This is a flag.", ] --rest=[ - cli.Option "rest-arg" + Option "rest-arg" --help="This is a rest argument." --multi, ] --examples=[ - cli.Example "Do something with the flag:" + Example "Do something with the flag:" --arguments="--some-option=foo --no-some-flag rest1 rest1", ] - --run=:: | app/cli.Application parsed/cli.Parsed | - print parsed["some-option"] - print parsed["some-flag"] - print parsed["rest-arg"] // A list. - app.ui.result "Computed result" + --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 ``` @@ -78,15 +78,15 @@ Subcommands are defined by adding a `Command` object as child to another command For example: ``` toit -import cli +import cli show * main args/List: - command := cli.Command "my-app" + command := Command "my-app" --help="My app does something." - sub := cli.Command "subcommand" + sub := Command "subcommand" --help="This is a subcommand." - --run=:: | app/cli.Application parsed/cli.Parsed | + --run=:: | invocation/Invocation | print "This is a subcommand." command.add sub @@ -117,17 +117,24 @@ of the library for a complete list. Users are encouraged to extend the `cli.Option` class and create their own typed options. -## Application +## Invocation A call to `command.run` parses the given arguments and then executes the -appropriate lambda. The lambda receives two arguments: an `Application` object and a -`Parsed` object. +appropriate lambda. The lambda receives one argument: an `Invocation` object. -The `Application` object contains getters for the cache, config, and UI objects. +The `Invocation` object contains: +- `cli`: A `Cli` object that contains common functionality for CLI applications, like + the `cache`, `config`, and `ui` 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. The + `Invocation` object has a shortcut operator `[]` that forwards to the `parameters`. +- `path`: A list of strings that contains the path to the command that was called. +- `command`: The command that was called. ### Cache -The cache is a simple key-value store that persists between runs. It is typically +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/`. 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. @@ -139,13 +146,12 @@ The cache can either store bytes, or handle paths to cached folders. The cache can store bytes. For example: ``` toit -import cli -import cli.cache as cli +import cli show Cli FileStore -store-bytes app/cli.Application: - cache := app.cache +store-bytes cli/Cli: + cache := cli.cache - data := cache.get "my-key": | store/cli.FileStore | + 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." @@ -158,14 +164,13 @@ The `FileStore` class provides convenience methods to store data. For example, i allows to store (either copy or move) existing files: ``` toit -import cli -import cli.cache as cli +import cli show Cli FileStore import host.file -store-from-file app/cli.Application: - cache := app.cache +store-from-file cli/Cli: + cache := cli.cache - data := cache.get "my-file-key": | store/cli.FileStore | + 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 | @@ -184,13 +189,13 @@ directory in the cache structure. The cache class has the `get-directory-path` method for this use case: ``` toit -import cli -import cli.cache as cli +import cli show Cli DirectoryStore +import host.file -store-directory app/cli.Application: - cache := app.cache +store-directory cli/Cli: + cache := cli.cache - directory := cache.get-directory-path "my-dir-key": | store/cli.DirectoryStore | + 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." @@ -216,11 +221,10 @@ strings, and the values can be any json-serializable object. When modifying a configuration it is necessary to `write` the changes back to disk. ``` toit -import cli -import cli.config as cli +import cli show Cli Config -config-example app/cli.Application: - config := app.config +config-example cli/Cli: + config := cli.config print "old value: $(config.get "my-key")" @@ -231,8 +235,8 @@ config-example app/cli.Application: Keys are split at "." to allow for nested values. For example: ``` toit -dotted-example app/cli.Application: - config := app.config +dotted-example cli/Cli: + config := cli.config print "old value: $(config.get "super-key.sub-key")" @@ -266,18 +270,18 @@ add the following options to the root command: --verbosity-level debug|info|verbose|quiet|silent Specify the verbosity level. (default: info) ``` -A corresponding UI object is then available in the `Application` object. Whenever the +A corresponding UI object is then available in the `Cli` object. Whenever the program wants to output something, it should use the `ui` object. ``` toit -import cli +import cli show Cli -some-chatty-method app/cli.Application: - ui := app.ui +some-chatty-method cli/Cli: + ui := cli.ui ui.debug "This is a debug message." ui.verbose "This is a verbose message." - ui.info "This is an info message." - ui.warning "This is a warning 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." @@ -303,22 +307,21 @@ Developers are encouraged to use the `ui.emit --structured` method to emit struc data. This is especially true for the result message. ``` toit -import cli -import cli.ui as cli +import cli show * main args: - cmd := cli.Command "my-app" + cmd := Command "my-app" --help="My app does something." - --run=:: | app/cli.Application parsed/cli.Parsed | - run-app app parsed + --run=:: run it -run-app app/cli.Application parsed/cli.Parsed: - ui := app.ui +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." ``` @@ -330,6 +333,9 @@ The `Ui` class has furthermore convenience methods to print tables, maps and lis 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. ## Features and bugs diff --git a/examples/cache.toit b/examples/cache.toit new file mode 100644 index 0000000..1a5ee57 --- /dev/null +++ b/examples/cache.toit @@ -0,0 +1,53 @@ +// Copyright (C) 2024 Toitware ApS. All rights reserved. +// Use of this source code is governed by an MIT-style license that can be +// found in the package's LICENSE file. + +import cli show * +import host.file + +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]. + +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. + +main args: + // Uses the application name "cli-example" which will be used + // to compute the path of the cache directory. + root-cmd := Command "cli-example" + --help=""" + An example application demonstrating the file-cache. + """ + --options=[ + OptionEnum "mode" ["file", "directory"] + --help="Store a file in the cache." + --required, + ] + --run=:: run it + root-cmd.run args + +run invocation/Invocation: + if invocation["mode"] == "file": + store-bytes invocation.cli + else: + store-directory invocation.cli diff --git a/examples/main.toit b/examples/commands.toit similarity index 65% rename from examples/main.toit rename to examples/commands.toit index 98152f1..3993247 100644 --- a/examples/main.toit +++ b/examples/commands.toit @@ -2,8 +2,7 @@ // Use of this source code is governed by an MIT-style license that can be // found in the package's LICENSE file. -import cli -import cli.ui as cli +import cli show * /** Creates a command-line executable that parses the command-line arguments and @@ -18,7 +17,7 @@ It can not be used to manage the fleet, for example by adding or removing devices. Usage: - examples/main.toit + examples/main.toit [] Commands: device Manage a particular device. @@ -26,17 +25,20 @@ Commands: status Shows the status of the fleet. Options: - -h, --help Show help for this command. + -h, --help Show help for this 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) Examples: - # Do a soft-reset of device 'foo': - fleet_manager device --device=foo reset -m soft + # Uploads the file 'foo.data' to the device 'foo': + main.toit device --device=foo upload foo.data # Show a detailed status of the fleet: - fleet_manager status --verbose + main.toit --verbose status - # Uploads the file 'foo.data' to the device 'foo': - fleet_manager device --device=foo upload foo.data + # Do a soft-reset of device 'foo': + main.toit device --device=foo reset -m soft ``` The help for the `reset` command looks as follows: @@ -61,17 +63,17 @@ Global options: Examples: # Do a soft-reset of device 'foo': - fleet_manager device --device=foo reset -m soft + main.toit device --device=foo reset -m soft # Do a hard-reset: - fleet_manager device reset --mode=hard + main.toit device reset --mode=hard ``` */ main arguments: // Creates a root command. // The name of the root command is not used. - root-cmd := cli.Command "fleet_manager" + root-cmd := Command "fleet_manager" --help=""" This is an imaginary fleet manager for a fleet of Toit devices. @@ -86,24 +88,25 @@ main arguments: // ============= Could be in a separate file status.toit. ============= -create-status-command -> cli.Command: - return cli.Command "status" +create-status-command -> Command: + return Command "status" --help="Shows the status of the fleet." --options=[ - cli.OptionInt "max-lines" --help="Maximum number of lines to show." --default=10, + OptionInt "max-lines" --help="Maximum number of lines to show." --default=10, ] --examples=[ - cli.Example "Show the status of the fleet:" --arguments="", - cli.Example "Show a detailed status of the fleet:" --arguments="--verbose" + Example "Show the status of the fleet:" --arguments="", + Example "Show a detailed status of the fleet:" --arguments="--verbose" --global-priority=7, // Show this example for the root command. ] - --run=:: | app parsed | fleet-status app parsed + --run=:: fleet-status it -fleet-status app/cli.Application parsed/cli.Parsed: - max-lines := parsed["max-lines"] - verbose := app.ui.level >= cli.Ui.VERBOSE-LEVEL +fleet-status invocation/Invocation: + max-lines := invocation["max-lines"] + cli := invocation.cli + verbose := cli.ui.level >= Ui.VERBOSE-LEVEL - app.ui.emit + cli.ui.emit --structured=: { "some": "json", "info": "about the status", @@ -114,8 +117,8 @@ fleet-status app/cli.Application parsed/cli.Parsed: // ============= Could be in a separate file device.toit. ============= -create-device-command -> cli.Command: - device-cmd := cli.Command "device" +create-device-command -> Command: + device-cmd := Command "device" // Aliases can be used to invoke this command. --aliases=[ "dev", @@ -128,80 +131,84 @@ create-device-command -> cli.Command: last used device is used. """ --options=[ - cli.Option "device" --short-name="d" + Option "device" --short-name="d" --help="The device to operate on." ] - device-cmd.add create-reset-command device-cmd.add create-upload-command + device-cmd.add create-reset-command return device-cmd -with-device app/cli.Application parsed/cli.Parsed [block]: - device := parsed["device"] - if not device: - device = app.config.get "default-device" - - if not device: - app.ui.abort "No device specified and no default device set." - - block.call device - -create-upload-command -> cli.Command: - return cli.Command "upload" +create-upload-command -> Command: + return Command "upload" --help=""" Uploads the given file to the device. Other useful information here. """ --rest=[ - cli.OptionString "data" + OptionString "data" --type="file" --help="The data to upload." --required, ] --examples=[ - cli.Example + Example "Uploads the file 'foo.data' to the device 'foo':" --arguments="--device=foo foo.data" --global-priority=8, // Include this example for super commands. ] - --run=:: | app parsed | upload-to-device app parsed + --run=:: upload-to-device it -upload-to-device app/cli.Application parsed/cli.Parsed: - data := parsed["data"] - - with-device app parsed: | device | - print "Uploading file '$data' to device '$device'." - -create-reset-command -> cli.Command: - return cli.Command "reset" +create-reset-command -> Command: + return Command "reset" --help=""" Resets the device. Other useful information here. """ --options=[ - cli.OptionEnum "mode" ["hard", "soft"] + OptionEnum "mode" ["hard", "soft"] --help="The reset mode to use." --short-name="m" --required, - cli.Flag "force" --short-name="f" + Flag "force" --short-name="f" --help="Force the reset even if the device is active.", ] --examples=[ - cli.Example + Example "Do a soft-reset of device 'foo':" --arguments="--device=foo -m soft" --global-priority=5, // Include this example for super commands. - cli.Example + Example "Do a hard-reset:" --arguments="--mode=hard", ] - --run=:: | app parsed | reset-device app parsed + --run=:: reset-device it + +with-device invocation/Invocation [block]: + cli := invocation.cli + + device := invocation["device"] + if not device: + device = cli.config.get "default-device" + + if not device: + cli.ui.abort "No device specified and no default device set." + + block.call device + +upload-to-device invocation/Invocation: + data := invocation["data"] + + with-device invocation: | device | + print "Uploading file '$data' to device '$device'." + +reset-device invocation/Invocation: + cli := invocation.cli -reset-device app/cli.Application parsed/cli.Parsed: - mode := parsed["mode"] - force := parsed["force"] + mode := invocation["mode"] + force := invocation["force"] - with-device app parsed: | device | - app.ui.info "Resetting device '$device' in $(mode)-mode." - if force: app.ui.debug "Using the force if necessary." + with-device invocation: | device | + cli.ui.inform "Resetting device '$device' in $(mode)-mode." + if force: cli.ui.debug "Using the force if necessary." diff --git a/examples/config.toit b/examples/config.toit new file mode 100644 index 0000000..f88139b --- /dev/null +++ b/examples/config.toit @@ -0,0 +1,35 @@ +// Copyright (C) 2024 Toitware ApS. All rights reserved. +// Use of this source code is governed by an MIT-style license that can be +// found in the package's LICENSE file. + +import cli show * + +config-example cli/Cli: + config := cli.config + + print "old value: $(config.get "my-key")" + + config["my-key"] = "my-value" + config.write + +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 + +main args: + // Uses the application name "cli-example" which will be used + // to compute the path of the config file. + root-cmd := Command "cli-example" + --help=""" + An example application demonstrating configurations. + """ + --run=:: run it + root-cmd.run args + +run invocation/Invocation: + config-example invocation.cli + dotted-example invocation.cli diff --git a/examples/ui.toit b/examples/ui.toit new file mode 100644 index 0000000..fed6d51 --- /dev/null +++ b/examples/ui.toit @@ -0,0 +1,70 @@ +// Copyright (C) 2024 Toitware ApS. All rights reserved. +// Use of this source code is governed by an MIT-style license that can be +// found in the package's LICENSE file. + +import cli show * + +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." + // By convention, 'result' calls should only happen in the method that + // initially received the Invocation object. + // For demonstration purposes, we call it here. + ui.result "This is a result message." + +emit-structured cli/Cli: + ui := cli.ui + ui.emit-list --kind=Ui.INFO --title="A list" [1, 2, 3] + ui.emit-map --kind=Ui.INFO --title="A map" { + "key": "value", + "key2": "value2", + } + ui.emit-table + --kind=Ui.INFO + --title="A table" + --header={"name": "Name", "age": "Age"} + [ + { "name": "Alice", "age": 25 }, + { "name": "Bob", "age": 30}, + ] + +main args: + // Uses the application name "cli-example" which will be used + // to compute the path of the config file. + root-cmd := Command "cli-example" + --help=""" + An example application demonstrating UI usage. + + Run with --verbosity-level={debug, info, verbose, quiet, silent}, or + --output-format={text, json} to see different output. + Note that '--output-format=json' redirects some output to stderr. + """ + --options=[ + Flag "chatty" --help="Run the chatty method" --default=false, + Flag "structured" --help="Output structured data" --default=false, + ] + --run=:: run it + root-cmd.run args + +run invocation/Invocation: + if invocation["chatty"]: + some-chatty-method invocation.cli + return + + if invocation["structured"]: + emit-structured invocation.cli + return + + ui := invocation.cli.ui + ui.emit --kind=Ui.RESULT + // 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." diff --git a/main.toit b/main.toit deleted file mode 100644 index f6d6ecb..0000000 --- a/main.toit +++ /dev/null @@ -1,69 +0,0 @@ -import .src.arg_parser as arguments - -main args: - command := arguments.Command "root" - --short_help="One-line help" - --aliases=["alias1", "alias2"] - --long_help=""" - Some help for this command. - """ - --options=[ - arguments.Flag "xhelp" --short_name="x" --short_help="xx" --required, - arguments.OptionEnum "format" ["long", "short"] - --short_name="f" - --short_help="Output format" - --default="long", - arguments.Option "global" --short_help="global required flag" --required, - ] - --examples=[ - arguments.Example "Example of subcommand" - --arguments="-x --format=long --global \"xxx\" other --other_int2=45 in out" - ] - - other := arguments.Command "other" - --aliases=["foo", "bar"] - --run=:: | parsed | run_other parsed - --long_help=""" - Some long help for the other command. - - Multiple lines. - """ - --options=[ - arguments.Option "other" --short_help="other option", - arguments.OptionInt "other_int" --short_help="other int option\ntwo lines" --default=42, - arguments.OptionInt "other_int2" --required --multi, - ] - --rest=[ - arguments.Option "input" --short_help="Input file" --required --type="file", - arguments.Option "output" --short_help="Output file" --multi --required --type="file", - ] - --examples=[ - arguments.Example "Use a long output format" - --arguments="-x --format=long --global \"xxx\" --other_int2=44 in out" - --global_priority=3, - ] - - other2 := arguments.Command "other2" - --run=:: | parsed | run_other parsed - --short_help="""Some short help for the other2 command.\nTwo lines.""" - --rest=[ - arguments.Option "input" --short_help="Input file" --required --type="file", - arguments.Option "output" --short_help="Output file" --multi --required --type="file", - arguments.Flag "flag_rest" - ] - --examples=[ - arguments.Example "other2 example" --arguments="-x --format=long --global \"xxx\" input output output2" --global_priority=5, - arguments.Example "other2 example2" --arguments="--no-xhelp --global xxx x y", - ] - - command.add other - command.add other2 - command.run args - -run_help parsed/arguments.Parsed: - print "in_help" - print parsed - -run_other parsed/arguments.Parsed: - print "in_other" - print parsed diff --git a/src/cli.toit b/src/cli.toit index 123a6c0..f961f2d 100644 --- a/src/cli.toit +++ b/src/cli.toit @@ -13,11 +13,58 @@ import .help-generator_ import .ui export Ui +export Cache FileStore DirectoryStore +export Config + +/** +An object giving access to common operations for CLI programs. + +If no ui is given uses $Ui.console. +*/ +interface Cli: + constructor + name/string + --ui/Ui?=null + --cache/Cache?=null + --config/Config?=null: + if not ui: ui = Ui.console + return Cli_ name --ui=ui --cache=cache --config=config + /** + The name of the application. + + Used to find configurations and caches. + */ + name -> string + + /** + The UI object to use for this application. + + Output should be written to this object. + */ + ui -> Ui + + /** The cache object for this application. */ + cache -> Cache + + /** The configuration object for this application. */ + config -> Config + + /** + Returns a new UI object based on the given arguments. + + All non-null arguments are used to create the UI object. If an argument is null, the + current value is used. + */ + with -> Cli + --name/string?=null + --ui/Ui?=null + --cache/Cache?=null + --config/Config?=null /** An object giving access to common operations for CLI programs. */ -class Application: +class Cli_ implements Cli: /** The name of the application. @@ -28,18 +75,38 @@ class Application: cache_/Cache? := null config_/Config? := null + /** + The UI object to use for this application. + + Output should be written to this object. + */ ui/Ui - constructor.private_ .name --.ui: + constructor .name --.ui --cache/Cache? --config/Config?: + cache_ = cache + config_ = config + /** The cache object for this application. */ cache -> Cache: if not cache_: cache_ = Cache --app-name=name return cache_ + /** The configuration object for this application. */ config -> Config: if not config_: config_ = Config --app-name=name return config_ + with -> Cli + --name/string?=null + --ui/Ui?=null + --cache/Cache?=null + --config/Config?=null: + return Cli_ + name or this.name + --ui=ui or this.ui + --cache=cache or cache_ + --config=config or config_ + /** A command. @@ -48,7 +115,7 @@ The main program is a command, and so are all subcommands. class Command: /** The name of the command. - The name of the root command is used as application name for the $Application. + The name of the root command is used as application name for the $Cli. */ name/string @@ -105,9 +172,9 @@ class Command: indented lines to continue paragraphs (just like toitdoc). The first paragraph of the $help is used as short help, and should have meaningful content on its own. - The $run callback is invoked when the command is executed. It is given the $Application and the - $Parsed object. If $run is null, then at least one subcommand must be added to this - command. + The $run callback is invoked when the command is executed. It is given an + $Invocation object. If $run is null, then at least one subcommand must be added + to this command. */ constructor name --usage/string?=null --help/string?=null --examples/List=[] \ --aliases/List=[] --options/List=[] --rest/List=[] --subcommands/List=[] --hidden/bool=false \ @@ -214,26 +281,28 @@ class Command: Runs this command. Parses the given $arguments and then invokes the command or one of its subcommands - with the $Parsed output. + with the $Invocation output. The $invoked-command is used only for the usage message in case of an error. It defaults to $system.program-name. - If no UI is given, the arguments are parsed for `--verbose`, `--verbosity-level` and - `--output-format` to create the appropriate UI object. If a $ui is given, then these - arguments are ignored. + If no $cli is given, the arguments are parsed for `--verbose`, `--verbosity-level` and + `--output-format` to create the appropriate UI object. If a $cli object is given, + then these arguments are ignored. The $add-ui-help flag is used to determine whether to include help for `--verbose`, ... - in the help output. By default it is active if no $ui is provided. + in the help output. By default it is active if no $cli is provided. */ - run arguments/List --invoked-command=system.program-name --ui/Ui?=null --add-ui-help/bool=(not ui) -> none: - if not ui: ui = create-ui-from-args_ arguments - if add-ui-help: - add-ui-options_ - app := Application.private_ name --ui=ui + run arguments/List --invoked-command=system.program-name --cli/Cli?=null --add-ui-help/bool=(not cli) -> none: + if not cli: + ui := create-ui-from-args_ arguments + if add-ui-help: + add-ui-options_ + cli = Cli_ name --ui=ui --cache=null --config=null parser := Parser_ --invoked-command=invoked-command - parsed := parser.parse this arguments - parsed.command.run-callback_.call app parsed + parser.parse this arguments: | path/List parameters/Parameters | + invocation := Invocation.private_ cli path parameters + invocation.command.run-callback_.call invocation add-ui-options_: has-output-format-option := false @@ -361,7 +430,7 @@ class Command: An option to a command. Options are used for any input from the command line to the program. They must have unique names, - so that they can be identified in the $Parsed output. + so that they can be identified in the $Invocation output. Non-rest options can be used with '--$name' or '-$short-name' (if provided). Rest options are positional and their name is not exposed to the user except for the help. @@ -425,7 +494,7 @@ abstract class Option: which is an alias for the $OptionString constructor. The $name sets the name of the option. It must be unique among all options of a command. - It is also used to extract the parsed value from the $Parsed object. For multi-word + It is also used to extract the parsed value from the $Invocation object. For multi-word options kebab case ('foo-bar') is recommended. The constructor automatically converts snake case ('foo_bar') to kebab case. This also means, that it's not possible to have two options that only differ in their case (kebab and snake). @@ -878,13 +947,51 @@ The result of parsing the command line arguments. An instance of this class is given to the command's `run` method. */ -class Parsed: +class Invocation: + /** + The $Cli object representing this application. + + It is common to pass this object to other functions and libraries. + */ + cli/Cli + /** A list of $Command objects, representing the commands that were given on the command line. The first command is the root command, the last command is the command that should be executed. */ path/List + /** + The parameters passed to the command. + */ + parameters/Parameters + + /** + Constructors a new invocation object. + */ + constructor.private_ .cli .path .parameters: + + /** + The command that should be executed. + */ + command -> Command: return path.last + + /** + Returns the value of the option with the given $name. + The $name must be an option of the command or one of its super commands. + + If the given $name is in snake_case, it is automatically converted + to kebab-case. + + This method is a shortcut for $parameters[name] ($Parameters.[]). + */ + operator[] name/string -> any: + return parameters[name] + +/** +The parameters passed to the command. +*/ +class Parameters: /** The parsed options. All options, including flags and rest arguments, are stored in this map. @@ -899,14 +1006,9 @@ class Parsed: seen-options_/Set /** - Builds a new $Parsed object. + Builds a new parameters object. */ - constructor.private_ .path .options_ .seen-options_: - - /** - The command that should be executed. - */ - command -> Command: return path.last + constructor.private_ .options_ .seen-options_: /** Returns the value of the option with the given $name. diff --git a/src/help-generator_.toit b/src/help-generator_.toit index b39ee05..ffce3a2 100644 --- a/src/help-generator_.toit +++ b/src/help-generator_.toit @@ -436,24 +436,25 @@ class HelpGenerator: // Parse it, to verify that it actually is valid. // We are also using the result to reorder the options. parser := Parser_ --invoked-command="root" --for-help-example - parsed/Parsed? := null + invocation-path/List? := null + invocation-parameters/Parameters? := null exception := catch: - parsed = parser.parse example-path.first command-line + parser.parse example-path.first command-line: | path parameters | + invocation-path = path + invocation-parameters = parameters if exception: throw "Error in example '$arguments-line': $exception" - parsed-path := parsed.path - // For each command, collect the options that are defined on it and that were // used in the example. option-to-command := {:} // Map from option to command. command-level := {:} flags := {} - for j := 0; j < parsed-path.size; j++: - current-command/Command := parsed-path[j] + for j := 0; j < invocation-path.size; j++: + current-command/Command := invocation-path[j] command-level[current-command] = j current-command.options_.do: | option/Option | - if not parsed.was-provided option.name: continue.do + if not invocation-parameters.was-provided option.name: continue.do option-to-command["--$option.name"] = current-command if option.short-name: option-to-command["-$option.short-name"] = current-command if option.is-flag: @@ -470,7 +471,7 @@ class HelpGenerator: if argument == "--": break if not argument.starts-with "-": - if path-index >= parsed-path.size - 1: + if path-index >= invocation-path.size - 1: argument-index-- break else: @@ -517,7 +518,7 @@ class HelpGenerator: list.add command-line[argument-index++] list - options-for-command.update parsed-path.last --init=(: []) : | list/List | + options-for-command.update invocation-path.last --init=(: []) : | list/List | list.add-all command-line[argument-index..] list @@ -528,7 +529,7 @@ class HelpGenerator: // For examples, we don't want the full path that was used to invoke the // command (like `build/bin/artemis`), but only the basename. app-name := basename_ invoked-command_ - parsed-path.do: | current-command | + invocation-path.do: | current-command | if is-root: is-root = false full-command.add app-name diff --git a/src/parser_.toit b/src/parser_.toit index ae4625f..6441323 100644 --- a/src/parser_.toit +++ b/src/parser_.toit @@ -42,7 +42,15 @@ class Parser_: ui.abort unreachable - parse root-command/Command arguments -> Parsed: + /** + Parses the command line $arguments and calls the given $block with + the result. + + Calls the $block with two arguments: + - The path (a list of $Command) to the command that was invoked. + - The $Parameters that were parsed. + */ + parse root-command/Command arguments/List [block] -> none: path := [] // Populate the options from the default values or empty lists (for multi-options) options := {:} @@ -64,10 +72,11 @@ class Parser_: seen-options.add option.name - create-help := : | arguments/List | - help-command := Command "help" --run=:: | app/Application _ | - help-command_ path arguments --invoked-command=invoked-command_ --ui=app.ui - Parsed.private_ [help-command] {:} {} + return-help := : | arguments/List | + help-command := Command "help" --run=:: | app/Invocation | + help-command_ path arguments --invoked-command=invoked-command_ --ui=app.cli.ui + block.call [help-command] (Parameters.private_ {:} {}) + return command/Command? := null set-command := : | new-command/Command | @@ -112,7 +121,7 @@ class Parser_: option := all-named-options.get kebab-name if not option: - if name == "help" and not is-inverted: return create-help.call [] + if name == "help" and not is-inverted: return-help.call [] fatal path "Unknown option: --$name" if option.is-flag and value != null: @@ -146,7 +155,7 @@ class Parser_: option-length++ if not option: - if short-name == "h": return create-help.call [] + if short-name == "h": return-help.call [] fatal path "Unknown option: -$short-name" i += option-length @@ -168,7 +177,7 @@ class Parser_: if not subcommand: if argument == "help" and command == root-command: // Special case for the help command. - return create-help.call arguments[index..] + return-help.call arguments[index..] fatal path "Unknown command: $argument" set-command.call subcommand @@ -198,4 +207,4 @@ class Parser_: if not command.run-callback_: fatal path "Missing subcommand." - return Parsed.private_ path options seen-options + block.call path (Parameters.private_ options seen-options) diff --git a/src/ui.toit b/src/ui.toit index e6e50ce..ea2ed53 100644 --- a/src/ui.toit +++ b/src/ui.toit @@ -118,6 +118,9 @@ abstract class PrinterBase implements Printer: emit-list --kind/int list/List --title/string?: indentation := print-prefix_ --kind=kind --title=title + emit-list_ list --indentation=indentation + + emit-list_ list/List --indentation/string: list.do: // TODO(florian): should the entries be recursively pretty-printed? print_ "$indentation$it" @@ -131,6 +134,9 @@ abstract class PrinterBase implements Printer: if value is Map: print_ "$indentation$key:" emit-map_ value --indentation="$indentation " + else if value is List: + print_ "$indentation$key:" + emit-list_ value --indentation="$indentation " else: // TODO(florian): should the entries handle lists as well. print_ "$indentation$key: $value" @@ -203,6 +209,9 @@ class Ui: constructor.json --level/int=NORMAL-LEVEL: return Ui --level=level --printer=JsonPrinter + constructor.from-args args/List: + return create-ui-from-args_ args + /** Emits the given $object using the $INFO kind. @@ -210,23 +219,27 @@ class Ui: As such, the object must be a valid JSON object. Otherwise, the $object is converted to a string. */ - info object/any: + inform object/any: emit --kind=INFO --structured=: object - /** Alias for $info. */ + /** Alias for $inform. */ print object/any: - info object + inform object - /** Variant of $info using the $DEBUG kind. */ + /** Variant of $inform using the $DEBUG kind. */ debug object/any: emit --kind=DEBUG --structured=: "$object" - /** Variant of $info using the $VERBOSE kind. */ + /** Variant of $inform using the $VERBOSE kind. */ verbose object/any: emit --kind=VERBOSE --structured=: "$object" + /** Variant of $verbose that only calls the block when necessary. */ + verbose [generator]: + do_ --kind=VERBOSE generator + /** Emits the given $object at a warning-level as a string. */ - warning object/any: + warn object/any: emit --kind=WARNING --structured=: "$object" /** Emits the given $object at an interactive-level as a string. */ @@ -315,14 +328,17 @@ class Ui: passes the result to the printer. If the printer does not request a structured representation calls the $text block and - passes the result as string to the printer. + passes the result as string to the printer. The $text block may return + null to indicate that no output should be generated. */ emit --kind/int=RESULT [--structured] [--text]: do_ --kind=kind: if printer_.needs-structured --kind=kind: printer_.emit-structured --kind=kind structured.call else: - printer_.emit --kind=kind "$text.call" + message := text.call + if message: + printer_.emit --kind=kind "$text.call" /** Variant of $(emit --kind [--structured] [--text]). @@ -337,6 +353,12 @@ class Ui: else: printer_.emit --kind=kind "$(structured.call)" + /** + Whether the UI wants a structured representation for the given $kind. + */ + wants-structured --kind/int=RESULT -> bool: + return printer_.needs-structured --kind=kind + /** Aborts the program with the given error message. @@ -347,6 +369,20 @@ class Ui: abort -> none: exit 1 + /** + Returns a new Ui object with the given $level and $printer. + + If $level is not provided, the level of the new Ui object is the same as + this object. + + If $printer is not provided, the printer of the new Ui object + is the same as this object. + */ + with --level/int?=null --printer/Printer?=null -> Ui: + return Ui + --level=level or this.level + --printer=printer or this.printer_ + /** Prints the given $str using $print. diff --git a/tests/check_test.toit b/tests/check_test.toit index 57e90a7..0774cb5 100644 --- a/tests/check_test.toit +++ b/tests/check_test.toit @@ -242,7 +242,7 @@ snake-kebab: cli.Option "foo-bar", cli.Option "foo_bar" ] - --run=:: | parsed/cli.Parsed | + --run=:: | parsed/cli.Invocation | unreachable expect-throw "Ambiguous option of 'root': --foo-bar.": root.check --invoked-command="root" diff --git a/tests/dashdash_test.toit b/tests/dashdash_test.toit index 605cabf..b6a2813 100644 --- a/tests/dashdash_test.toit +++ b/tests/dashdash_test.toit @@ -11,11 +11,11 @@ main: cli.Option "first" --required, cli.Option "arg" --multi ] - --run=:: | _ parsed| test-dashdash parsed + --run=:: test-dashdash it root.run ["--", "prog", "arg1", "arg2", "arg3"] -test-dashdash parsed/cli.Parsed: - first := parsed["first"] - rest := parsed["arg"] +test-dashdash invocation/cli.Invocation: + first := invocation["first"] + rest := invocation["arg"] expect-equals "prog" first expect-list-equals ["arg1", "arg2", "arg3"] rest diff --git a/tests/health/readme1.toit b/tests/health/readme1.toit index d6852ea..9ecc9a8 100644 --- a/tests/health/readme1.toit +++ b/tests/health/readme1.toit @@ -2,32 +2,32 @@ // Use of this source code is governed by a Zero-Clause BSD license that can // be found in the tests/LICENSE file. -import cli +import cli show * main args/List: - command := cli.Command "my-app" + command := Command "my-app" --help="My app does something." --options=[ - cli.Option "some-option" + Option "some-option" --help="This is an option." --required, - cli.Flag "some-flag" + Flag "some-flag" --short-name="f" --help="This is a flag.", ] --rest=[ - cli.Option "rest-arg" + Option "rest-arg" --help="This is a rest argument." --multi, ] --examples=[ - cli.Example "Do something with the flag:" + Example "Do something with the flag:" --arguments="--some-option=foo --no-some-flag rest1 rest1", ] - --run=:: | app/cli.Application parsed/cli.Parsed | - print parsed["some-option"] - print parsed["some-flag"] - print parsed["rest-arg"] // A list. - app.ui.result "Computed result" + --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 diff --git a/tests/health/readme2.toit b/tests/health/readme2.toit index bd95c3d..88c7afa 100644 --- a/tests/health/readme2.toit +++ b/tests/health/readme2.toit @@ -2,15 +2,15 @@ // Use of this source code is governed by a Zero-Clause BSD license that can // be found in the tests/LICENSE file. -import cli +import cli show * main args/List: - command := cli.Command "my-app" + command := Command "my-app" --help="My app does something." - sub := cli.Command "subcommand" + sub := Command "subcommand" --help="This is a subcommand." - --run=:: | app/cli.Application parsed/cli.Parsed | + --run=:: | invocation/Invocation | print "This is a subcommand." command.add sub diff --git a/tests/health/readme3.toit b/tests/health/readme3.toit index c0288bf..b530988 100644 --- a/tests/health/readme3.toit +++ b/tests/health/readme3.toit @@ -2,39 +2,15 @@ // Use of this source code is governed by a Zero-Clause BSD license that can // be found in the tests/LICENSE file. -import cli -import cli.cache as cli -import host.file +import cli show Cli FileStore -store-bytes app/cli.Application: - cache := app.cache +store-bytes cli/Cli: + cache := cli.cache - data := cache.get "my-key": | store/cli.FileStore | + 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]. - -store-from-file app/cli.Application: - cache := app.cache - - data := cache.get "my-file-key": | store/cli.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". - -main args: - cmd := cli.Command "my-app" - --run=:: | app/cli.Application parsed/cli.Parsed | - print "Data is cached in $app.cache.path" - store-bytes app - store-from-file app - - cmd.run args diff --git a/tests/health/readme4.toit b/tests/health/readme4.toit index 77d3a42..12bf0d3 100644 --- a/tests/health/readme4.toit +++ b/tests/health/readme4.toit @@ -2,29 +2,19 @@ // Use of this source code is governed by a Zero-Clause BSD license that can // be found in the tests/LICENSE file. -import cli -import cli.cache as cli +import cli show Cli FileStore import host.file -store-directory app/cli.Application: - cache := app.cache +store-from-file cli/Cli: + cache := cli.cache - directory := cache.get-directory-path "my-dir-key": | store/cli.DirectoryStore | + data := cache.get "my-file-key": | store/FileStore | // 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." + print "Data 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 + 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 directory // Prints the path to the directory. - -main args: - cmd := cli.Command "my-app" - --run=:: | app/cli.Application parsed/cli.Parsed | - print "Data is cached in $app.cache.path" - store-directory app - - cmd.run args + print data // Prints the binary representation of "Hello world". diff --git a/tests/health/readme5.toit b/tests/health/readme5.toit index fc911c0..dfe1e3f 100644 --- a/tests/health/readme5.toit +++ b/tests/health/readme5.toit @@ -2,30 +2,20 @@ // Use of this source code is governed by a Zero-Clause BSD license that can // be found in the tests/LICENSE file. -import cli -import cli.config as cli - -config-example app/cli.Application: - config := app.config - - print "old value: $(config.get "my-key")" - - config["my-key"] = "my-value" - config.write - -dotted-example app/cli.Application: - config := app.config - - print "old value: $(config.get "super-key.sub-key")" - - config["super-key.sub-key"] = "my-value" - config.write - -main args: - cmd := cli.Command "my-app" - --run=:: | app/cli.Application parsed/cli.Parsed | - print "Configuration is stored in $app.config.path" - config-example app - dotted-example app - - cmd.run args +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. diff --git a/tests/health/readme6.toit b/tests/health/readme6.toit new file mode 100644 index 0000000..4e41515 --- /dev/null +++ b/tests/health/readme6.toit @@ -0,0 +1,13 @@ +// Copyright (C) 2024 Toitware ApS. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +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 diff --git a/tests/health/readme7.toit b/tests/health/readme7.toit new file mode 100644 index 0000000..8f2aaed --- /dev/null +++ b/tests/health/readme7.toit @@ -0,0 +1,13 @@ +// Copyright (C) 2024 Toitware ApS. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +import cli show Cli Config + +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 diff --git a/tests/health/readme8.toit b/tests/health/readme8.toit new file mode 100644 index 0000000..6223524 --- /dev/null +++ b/tests/health/readme8.toit @@ -0,0 +1,15 @@ +// Copyright (C) 2024 Toitware ApS. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +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." diff --git a/tests/health/readme9.toit b/tests/health/readme9.toit new file mode 100644 index 0000000..c4339bb --- /dev/null +++ b/tests/health/readme9.toit @@ -0,0 +1,20 @@ +// Copyright (C) 2024 Toitware ApS. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +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." diff --git a/tests/help_test.toit b/tests/help_test.toit index 0213d43..8bff2bf 100644 --- a/tests/help_test.toit +++ b/tests/help_test.toit @@ -19,7 +19,8 @@ main: check-output expected/string [block]: ui := TestUi - block.call ui + cli := cli.Cli "test" --ui=ui + block.call cli all-output := ui.messages.join "\n" if expected != all-output and expected.size == all-output.size: for i := 0; i < expected.size; i++: @@ -70,8 +71,8 @@ test-combination: # Example 1: app --option1 foo rest """ - check-output cmd-help: | ui/cli.Ui | - cmd.run ["--help"] --ui=ui --invoked-command="bin/app" + check-output cmd-help: | cli/cli.Cli | + cmd.run ["--help"] --cli=cli --invoked-command="bin/app" expect-equals cmd-help (cmd.help --invoked-command="bin/app") sub := cli.Command "sub" @@ -118,8 +119,8 @@ test-combination: # Sub Example 2: app --option1 foo sub --option_sub1='xyz' """ - check-output cmd-help: | ui/cli.Ui | - cmd.run ["--help"] --ui=ui --invoked-command="bin/app" + check-output cmd-help: | cli/cli.Cli | + cmd.run ["--help"] --cli=cli --invoked-command="bin/app" expect-equals cmd-help (cmd.help --invoked-command="bin/app") @@ -148,8 +149,8 @@ test-combination: app --option1 foo sub --option_sub1='xyz' """ - check-output sub-help: | ui/cli.Ui | - cmd.run ["help", "sub"] --ui=ui --invoked-command="bin/app" + check-output sub-help: | cli/cli.Cli | + cmd.run ["help", "sub"] --cli=cli --invoked-command="bin/app" test-usage: build-usage := : | path/List | diff --git a/tests/parser_test.toit b/tests/parser_test.toit index f0872be..7ecd4b4 100644 --- a/tests/parser_test.toit +++ b/tests/parser_test.toit @@ -7,9 +7,9 @@ import expect show * import .test-ui -check-arguments expected/Map parsed/cli.Parsed: +check-arguments expected/Map invocation/cli.Invocation: expected.do: | key value | - expect-equals value parsed[key] + expect-equals value invocation[key] main: test-options @@ -33,8 +33,7 @@ test-options: cli.Option "bar" --short-name="b", cli.OptionInt "gee" --short-name="g", ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed + --run=:: check-arguments expected it expected = {"foo": "foo_value", "bar": "bar_value", "gee": null} cmd.run ["-f", "foo_value", "-b", "bar_value"] @@ -57,8 +56,7 @@ test-options: cli.Flag "bar" --short-name="b", cli.Option "fizz" --short-name="iz" --default="default_fizz", ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed + --run=:: check-arguments expected it expected = {"foo": "default_foo", "bar": true, "fizz": "default_fizz"} cmd.run ["-b"] @@ -87,8 +85,7 @@ test-options: --options=[ cli.Flag "foo" --short-name="f" --default=false, ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed + --run=:: check-arguments expected it expected = {"foo": false} cmd.run [] @@ -103,8 +100,7 @@ test-options: --options=[ cli.Flag "foo" --short-name="f" --default=true, ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed + --run=:: check-arguments expected it expected = {"foo": true } @@ -117,8 +113,7 @@ test-multi: cli.Option "foo" --short-name="f" --multi, cli.Option "bar" --short-name="b" --multi --split-commas, ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed + --run=:: check-arguments expected it expected = {"foo": ["foo_value"], "bar": ["bar_value"]} cmd.run ["-f", "foo_value", "-b", "bar_value"] @@ -147,8 +142,7 @@ test-multi: --options=[ cli.OptionInt "foo" --short-name="f" --multi --split-commas, ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed + --run=:: check-arguments expected it expected = {"foo": [1, 2, 3]} cmd.run ["-f", "1", "-f", "2", "-f", "3"] @@ -158,8 +152,7 @@ test-multi: --options=[ cli.Flag "foo" --short-name="f" --multi, ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed + --run=:: check-arguments expected it expected = {"foo": [true, true, true]} cmd.run ["-f", "-f", "-f"] @@ -170,8 +163,7 @@ test-multi: --options=[ cli.OptionEnum "foo" ["a", "b"] --short-name="f" --multi, ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed + --run=:: check-arguments expected it expected = {"foo": ["a", "b", "a"]} cmd.run ["-f", "a", "-f", "b", "-f", "a"] @@ -183,8 +175,7 @@ test-rest: cli.Option "foo", cli.OptionInt "bar", ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed + --run=:: check-arguments expected it expected = {"foo": "foo_value", "bar": 42} cmd.run ["foo_value", "42"] @@ -200,8 +191,7 @@ test-rest: cli.Option "foo" --required, cli.OptionInt "bar" --default=42, ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed + --run=:: check-arguments expected it expected = {"foo": "foo_value", "bar": 42} cmd.run ["foo_value"] @@ -209,16 +199,15 @@ test-rest: expected = {"foo": "foo_value", "bar": 43} cmd.run ["foo_value", "43"] - expect-abort "Missing required rest argument: 'foo'.": | ui/cli.Ui | - cmd.run [] --ui=ui + expect-abort "Missing required rest argument: 'foo'.": | app/cli.Cli | + cmd.run [] --cli=app cmd = cli.Command "test" --rest=[ cli.Option "foo" --required, cli.Option "bar" --required --multi, ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed + --run=:: check-arguments expected it expected = {"foo": "foo_value", "bar": ["bar_value"]} cmd.run ["foo_value", "bar_value"] @@ -226,13 +215,13 @@ test-rest: expected = {"foo": "foo_value", "bar": ["bar_value", "bar_value2"]} cmd.run ["foo_value", "bar_value", "bar_value2"] - expect-abort "Missing required rest argument: 'bar'.": | ui/cli.Ui | - cmd.run ["foo_value"] --ui=ui + expect-abort "Missing required rest argument: 'bar'.": | app/cli.Cli | + cmd.run ["foo_value"] --cli=app cmd = cli.Command "test" --run=:: null - expect-abort "Unexpected rest argument: 'baz'.": | ui/cli.Ui | - cmd.run ["baz"] --ui=ui + expect-abort "Unexpected rest argument: 'baz'.": | app/cli.Cli | + cmd.run ["baz"] --cli=app test-subcommands: expected/Map? := null @@ -242,17 +231,14 @@ test-subcommands: --options=[ cli.Option "foo" --short-name="f", ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed, + --run=:: check-arguments expected it, cli.Command "sub2" --options=[ cli.Option "bar" --short-name="b", ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed, + --run=:: check-arguments expected it ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed + --run=:: check-arguments expected it expected = {"foo": "foo_value"} cmd.run ["sub1", "-f", "foo_value"] @@ -270,8 +256,8 @@ test-subcommands: test-no-option: cmd := cli.Command "test" - --run=:: | app/cli.Application parsed/cli.Parsed | - expect-throw "No option named 'foo'": parsed["foo"] + --run=:: | invocation/cli.Invocation | + expect-throw "No option named 'foo'": invocation["foo"] cmd.run [] cmd = cli.Command "test" @@ -283,8 +269,8 @@ test-no-option: --options=[ cli.Option "bar" --short-name="b", ] - --run=:: | app/cli.Application parsed/cli.Parsed | - expect-throw "No option named 'gee'": parsed["gee"], + --run=:: | invocation/cli.Invocation | + expect-throw "No option named 'gee'": invocation["gee"], ] cmd.run ["sub1", "-b", "bar_value"] @@ -296,8 +282,7 @@ test-invert-flag: --options=[ cli.Flag "foo" --short-name="f", ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed + --run=:: check-arguments expected it expected = {"foo": null} cmd.run [] @@ -312,8 +297,7 @@ test-invert-flag: --options=[ cli.Flag "foo" --short-name="f" --default=true, ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed + --run=:: check-arguments expected it expected = {"foo": true} cmd.run [] @@ -326,11 +310,11 @@ test-invert-non-flag: --options=[ cli.Option "foo" --short-name="f", ] - --run=:: | app/cli.Application parsed/cli.Parsed | + --run=:: | invocation/cli.Invocation | unreachable - expect-abort "Cannot invert non-boolean flag --foo.": | ui/cli.Ui | - cmd.run ["--no-foo"] --ui=ui + expect-abort "Cannot invert non-boolean flag --foo.": | app/cli.Cli | + cmd.run ["--no-foo"] --cli=app test-value-for-flag: expected/Map? := null @@ -338,8 +322,8 @@ test-value-for-flag: --options=[ cli.Flag "foo" --short-name="f", ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments expected parsed + --run=:: | invocation/cli.Invocation | + check-arguments expected invocation expected = {"foo": true} cmd.run ["--foo=true"] @@ -348,22 +332,22 @@ test-value-for-flag: cmd.run ["--foo=false"] ["on", "off", "yes", "no"].do: | value | - expect-abort "Invalid value for boolean flag 'foo': '$value'. Valid values are: true, false.": | ui/cli.Ui | - cmd.run ["--foo=$value"] --ui=ui + expect-abort "Invalid value for boolean flag 'foo': '$value'. Valid values are: true, false.": | app/cli.Cli | + cmd.run ["--foo=$value"] --cli=app test-missing-args: cmd := cli.Command "test" --options=[ cli.Option "foo" --short-name="f", ] - --run=:: | app/cli.Application parsed/cli.Parsed | + --run=:: | invocation/cli.Invocation | unreachable - expect-abort "Option --foo requires an argument.": | ui/cli.Ui | - cmd.run ["--foo"] --ui=ui + expect-abort "Option --foo requires an argument.": | app/cli.Cli | + cmd.run ["--foo"] --cli=app - expect-abort "Option -f requires an argument.": | ui/cli.Ui | - cmd.run ["-f"] --ui=ui + expect-abort "Option -f requires an argument.": | app/cli.Cli | + cmd.run ["-f"] --cli=app test-missing-subcommand: cmd := cli.Command "test" @@ -372,16 +356,16 @@ test-missing-subcommand: --run=:: unreachable ] - expect-abort "Missing subcommand.": | ui/cli.Ui | - cmd.run [] --ui=ui + expect-abort "Missing subcommand.": | app/cli.Cli | + cmd.run [] --cli=app test-dash-arg: cmd := cli.Command "test" --options=[ cli.Option "foo" --short-name="f", ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments {"foo": "-"} parsed + --run=:: | invocation/cli.Invocation | + check-arguments {"foo": "-"} invocation cmd.run ["-f", "-"] @@ -396,8 +380,8 @@ test-mixed-rest-named: --rest=[ cli.Option "baz" --required, ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments {"foo": "foo_value", "bar": "bar_value", "baz": "baz_value"} parsed + --run=:: | invocation/cli.Invocation | + check-arguments {"foo": "foo_value", "bar": "bar_value", "baz": "baz_value"} invocation cmd.run ["--foo", "foo_value", "--bar", "bar_value", "baz_value"] cmd.run ["baz_value", "--foo", "foo_value", "--bar", "bar_value"] @@ -411,8 +395,8 @@ test-mixed-rest-named: --rest=[ cli.Option "baz" --required, ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments {"foo": "foo_value", "bar": "bar_value", "baz": "--foo"} parsed + --run=:: | invocation/cli.Invocation | + check-arguments {"foo": "foo_value", "bar": "bar_value", "baz": "--foo"} invocation // Because of the '--', the rest argument is not interpreted as a named argument. cmd.run ["--foo", "foo_value", "--bar", "bar_value", "--", "--foo"] @@ -423,9 +407,9 @@ test-snake-kebab: cli.Option "foo-bar" --short-name="f", cli.Option "toto_titi" ] - --run=:: | app/cli.Application parsed/cli.Parsed | - check-arguments {"foo-bar": "foo_value", "toto-titi": "toto_value" } parsed - check-arguments {"foo_bar": "foo_value", "toto_titi": "toto_value" } parsed + --run=:: | invocation/cli.Invocation | + check-arguments {"foo-bar": "foo_value", "toto-titi": "toto_value" } invocation + check-arguments {"foo_bar": "foo_value", "toto_titi": "toto_value" } invocation cmd.run ["--foo-bar", "foo_value", "--toto-titi", "toto_value"] cmd.run ["--foo_bar", "foo_value", "--toto_titi", "toto_value"] diff --git a/tests/subcommand_test.toit.toit b/tests/subcommand_test.toit.toit index 4bfdc5e..c7f37d4 100644 --- a/tests/subcommand_test.toit.toit +++ b/tests/subcommand_test.toit.toit @@ -5,7 +5,7 @@ import cli import expect show * -check-arguments expected/Map parsed/cli.Parsed: +check-arguments expected/Map parsed/cli.Invocation: expected.do: | key value | expect-equals value parsed[key] diff --git a/tests/test_ui.toit b/tests/test_ui.toit index 4b5cf2c..6296fbc 100644 --- a/tests/test_ui.toit +++ b/tests/test_ui.toit @@ -2,6 +2,7 @@ // Use of this source code is governed by a Zero-Clause BSD license that can // be found in the tests/LICENSE file. +import cli as cli import cli.ui as cli import cli.parser_ as cli-parser import expect show * @@ -33,8 +34,9 @@ class TestPrinter extends cli.PrinterBase: expect-abort expected/string [block]: ui := TestUi + cli := cli.Cli "test" --ui=ui exception := catch: - block.call ui + block.call cli expect-equals "abort" exception all-output := ui.messages.join "\n" if not all-output.starts-with "Error: $expected": diff --git a/tests/ui-test.toit b/tests/ui-test.toit index f00ebc6..6d3bd3b 100644 --- a/tests/ui-test.toit +++ b/tests/ui-test.toit @@ -51,11 +51,11 @@ test-console: printer := TestPrinter --no-needs-structured ui := Ui --printer=printer - ui.info "hello" + ui.inform "hello" expect-equals "hello\n" printer.stdout printer.reset - ui.info ["hello", "world"] + ui.inform ["hello", "world"] expect-equals "[hello, world]\n" printer.stdout printer.reset @@ -118,7 +118,7 @@ test-console: """ printer.stdout printer.reset - ui.info { + ui.inform { "a": "b", "c": "d", } @@ -155,7 +155,7 @@ test-console: expect-equals "foo\n" printer.stdout printer.reset - ui.warning "foo" + ui.warn "foo" expect-equals "Warning: foo\n" printer.stdout printer.reset @@ -175,7 +175,7 @@ test-structured: printer := TestPrinter --needs-structured ui := Ui --printer=printer - ui.info "hello" + ui.inform "hello" expect-equals ["hello"] printer.structured printer.reset @@ -192,7 +192,7 @@ test-structured: "foo", "bar", ] - ui.info list + ui.inform list expect-equals 1 printer.structured.size expect-identical list printer.structured[0] printer.reset @@ -217,7 +217,7 @@ test-json: // Anything that isn't a result is emitted on stderr as if it was // a console Ui. - ui.info "hello" + ui.inform "hello" expect-equals "hello\n" printer.stderr printer.reset @@ -232,7 +232,7 @@ test-json: } expect-equals "{\"foo\":1,\"bar\":2}" printer.stdout - ui.warning "some warning" + ui.warn "some warning" expect-equals "Warning: some warning\n" printer.stderr printer.reset