Skip to content

Commit

Permalink
Provide 'app' to '--run' lambda. (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
floitsch authored Nov 22, 2023
1 parent ab9d84e commit 80c1692
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 89 deletions.
54 changes: 36 additions & 18 deletions examples/main.toit
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the package's LICENSE file.
import cli
import cli.ui as cli

/**
Creates a command-line executable that parses the command-line arguments and
Expand Down Expand Up @@ -53,7 +54,10 @@ Options:
-m, --mode hard|soft The reset mode to use. (required)
Global options:
-d, --device string The device to operate on.
-d, --device string The device to operate on.
--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':
Expand Down Expand Up @@ -86,24 +90,28 @@ create-status-command -> cli.Command:
return cli.Command "status"
--help="Shows the status of the fleet:"
--options=[
cli.Flag "verbose" --short-name="v" --help="Show more details." --multi,
cli.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"
--global-priority=7, // Show this example for the root command.
]
--run=:: fleet-status it
--run=:: | app parsed | fleet-status app parsed

fleet-status parsed/cli.Parsed:
verbose-list := parsed["verbose"]
trues := (verbose-list.filter: it).size
falses := verbose-list.size - trues
verbose-level := trues - falses
fleet-status app/cli.App parsed/cli.Parsed:
max-lines := parsed["max-lines"]
verbose := app.ui.level >= cli.Ui.VERBOSE-LEVEL

print "Max $max-lines of status with verbosity-level $verbose-level."
app.ui.do --kind=cli.Ui.RESULT: | printer/cli.Printer |
printer.emit-structured
--json=:
{
"some": "json",
"info": "about the status",
}
--stdout=:
printer.emit "Printing max $max-lines of status. (verbose: $(verbose ? "yes" : "no"))"


// ============= Could be in a separate file device.toit. =============
Expand All @@ -129,6 +137,16 @@ create-device-command -> cli.Command:
device-cmd.add create-upload-command
return device-cmd

with-device app/cli.App 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"
--help="""
Expand All @@ -148,13 +166,13 @@ create-upload-command -> cli.Command:
--arguments="--device=foo foo.data"
--global-priority=8, // Include this example for super commands.
]
--run=:: upload-to-device it
--run=:: | app parsed | upload-to-device app parsed

upload-to-device parsed/cli.Parsed:
device := parsed["device"]
upload-to-device app/cli.App parsed/cli.Parsed:
data := parsed["data"]

print "Uploading file '$data' to device '$device'."
with-device app parsed: | device |
print "Uploading file '$data' to device '$device'."

create-reset-command -> cli.Command:
return cli.Command "reset"
Expand All @@ -180,12 +198,12 @@ create-reset-command -> cli.Command:
"Do a hard-reset:"
--arguments="--mode=hard",
]
--run=:: reset-device it
--run=:: | app parsed | reset-device app parsed

reset-device parsed/cli.Parsed:
device := parsed["device"]
reset-device app/cli.App parsed/cli.Parsed:
mode := parsed["mode"]
force := parsed["force"]

print "Resetting device '$device' in $(mode)-mode."
if force: print "Using the force if necessary."
with-device app parsed: | device |
app.ui.info "Resetting device '$device' in $(mode)-mode."
if force: app.ui.debug "Using the force if necessary."
16 changes: 16 additions & 0 deletions examples/package.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
sdk: ^2.0.0-alpha.120
prefixes:
cli: ..
packages:
..:
path: ..
prefixes:
fs: pkg-fs
host: pkg-host
pkg-fs:
url: github.com/toitlang/pkg-fs
name: fs
version: 1.0.0
hash: c816c85022a155f37a4396455da9b27595050de1
prefixes:
host: pkg-host
pkg-host:
url: github.com/toitlang/pkg-host
name: host
version: 1.11.0
hash: 7e7df6ac70d98a02f232185add81a06cec0d77e8
90 changes: 84 additions & 6 deletions src/cli.toit
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,41 @@
import uuid

import .cache
import .config
import .parser_
import .utils_
import .help-generator_
import .ui

export Ui

/**
An object giving access to common operations for CLI programs.
*/
class App:
/**
The name of the application.
Used to find configurations and caches.
*/
name/string

cache_/Cache? := null
config_/Config? := null

ui/Ui

constructor.private_ .name --.ui:

cache -> Cache:
if not cache_: cache_ = Cache --app-name=name
return cache_

config -> Config:
if not config_: config_ = Config --app-name=name
return config_

/**
A command.
Expand All @@ -19,7 +47,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 usually ignored (and replaced by the executable name).
The name of the root command is used as application name for the $App.
*/
name/string

Expand All @@ -43,7 +71,7 @@ class Command:
aliases_/List

/** Options to the command. */
options_/List
options_/List := ?

/** The rest arguments. */
rest_/List
Expand Down Expand Up @@ -75,6 +103,10 @@ class Command:
The $help is a longer description of the command that can span multiple lines. Use
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 $App and the
$Parsed 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 \
Expand Down Expand Up @@ -170,12 +202,58 @@ class Command:
The $invoked-command is used only for the usage message in case of an
error. It defaults to $program-name.
The default $ui prints to stdout and calls `exit 1` when $Ui.abort is called.
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.
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.
*/
run arguments/List --invoked-command=program-name --ui/Ui=ConsoleUi -> none:
parser := Parser_ --ui=ui --invoked-command=invoked-command
run arguments/List --invoked-command=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 := App.private_ name --ui=ui
parser := Parser_ --invoked-command=invoked-command
parsed := parser.parse this arguments
parsed.command.run-callback_.call parsed
parsed.command.run-callback_.call app parsed

add-ui-options_:
has-output-format-option := false
has-verbose-flag := false
has-verbosity-level-option := false

options_.do: | option/Option |
if option.name == "output-format": has-output-format-option = true
if option.name == "verbose": has-verbose-flag = true
if option.name == "verbosity-level": has-verbosity-level-option = true

is-copied := false
if not has-output-format-option:
options_ = options_.copy
is-copied = true
option := OptionEnum "output-format"
["text", "json"]
--help="Specify the format used when printing to the console."
--default="text"
options_.add option
if not has-verbose-flag:
if not is-copied:
options_ = options_.copy
is-copied = true
option := Flag "verbose"
--help="Enable verbose output. Shorthand for --verbosity-level=verbose."
--default=false
options_.add option
if not has-verbosity-level-option:
if not is-copied:
options_ = options_.copy
is-copied = true
option := OptionEnum "verbosity-level"
["debug", "info", "verbose", "quiet", "silent"]
--help="Specify the verbosity level."
--default="info"
options_.add option

/**
Checks this command and all subcommands for errors.
Expand Down
20 changes: 7 additions & 13 deletions src/help-generator_.toit
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ help-command_ path/List arguments/List --invoked-command/string --ui/Ui:

subcommand := command.find-subcommand_ argument
if not subcommand:
ui.print "Unknown command: $argument"
ui.abort
ui.abort "Unknown command: $argument"
unreachable
command = subcommand
path.add command
Expand Down Expand Up @@ -393,8 +392,12 @@ class HelpGenerator:

// Parse it, to verify that it actually is valid.
// We are also using the result to reorder the options.
parser := Parser_ --ui=(ExampleUi_ arguments-line) --invoked-command="root" --no-usage-on-error
parsed := parser.parse example-path.first command-line --for-help-example
parser := Parser_ --invoked-command="root" --for-help-example
parsed/Parsed? := null
exception := catch:
parsed = parser.parse example-path.first command-line
if exception:
throw "Error in example '$arguments-line': $exception"

parsed-path := parsed.path

Expand Down Expand Up @@ -609,12 +612,3 @@ class HelpGenerator:

to-string -> string:
return buffer_.join ""


class ExampleUi_ extends ConsoleUi:
example_/string

constructor .example_:

abort:
throw "Error in example: $example_"
57 changes: 42 additions & 15 deletions src/parser_.toit
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,54 @@
import .cli
import .help-generator_
import .ui
import .utils_
import host.pipe

class StderrUi_ extends Ui:
constructor:
super --level=Ui.NORMAL-LEVEL
create-printer_ prefix/string? kind/int -> Printer:
return StderrPrinter_ prefix

class StderrPrinter_ extends PrinterBase:
constructor prefix/string?:
super prefix

needs-structured_: return false
handle-structured_ o: unreachable
print_ o:
pipe.stderr.write "$o\n"

test-ui_/Ui? := null

class Parser_:
ui_/Ui
invoked-command_/string
usage-on-error_/bool
for-help-example_/bool

constructor --ui/Ui --invoked-command/string --usage-on-error=true:
ui_ = ui
constructor --invoked-command/string --for-help-example/bool=false:
invoked-command_ = invoked-command
usage-on-error_ = usage-on-error
for-help-example_ = for-help-example

/**
Reports and error and aborts the program.
The program was called with wrong arguments.
*/
fatal path/List str/string:
ui_.print "Error: $str"
if usage-on-error_:
ui_.print ""
help-command_ path [] --invoked-command=invoked-command_ --ui=ui_
ui_.abort
if for-help-example_:
throw str

// If there is a test-ui_ use it.
// Otherwise, ignore the ui that was determined through the command line and
// print the usage on stderr, followed by an exit 1.
ui := test-ui_ or StderrUi_
ui.error str
help-command_ path [] --invoked-command=invoked-command_ --ui=ui
ui.abort
unreachable

parse root-command/Command arguments --for-help-example/bool=false -> Parsed:
parse root-command/Command arguments -> Parsed:
path := []
// Populate the options from the default values or empty lists (for multi-options)
options := {:}
Expand All @@ -36,19 +63,19 @@ class Parser_:
add-option := : | option/Option argument/string |
if option.is-multi:
values := option.should-split-commas ? argument.split "," : [argument]
parsed := values.map: option.parse it --for-help-example=for-help-example
parsed := values.map: option.parse it --for-help-example=for-help-example_
options[option.name].add-all parsed
else if seen-options.contains option.name:
fatal path "Option was provided multiple times: $option.name"
else:
value := option.parse argument --for-help-example=for-help-example
value := option.parse argument --for-help-example=for-help-example_
options[option.name] = value

seen-options.add option.name

create-help := : | arguments/List |
help-command := Command "help" --run=::
help-command_ path arguments --invoked-command=invoked-command_ --ui=ui_
help-command := Command "help" --run=:: | app/App _ |
help-command_ path arguments --invoked-command=invoked-command_ --ui=app.ui
Parsed.private_ [help-command] {:} {}

command/Command? := null
Expand Down
Loading

0 comments on commit 80c1692

Please sign in to comment.