Skip to content

Commit

Permalink
Refactor and minor features.
Browse files Browse the repository at this point in the history
Prepares for a v2.
  • Loading branch information
floitsch committed Jul 30, 2024
1 parent 4adb30b commit 00e2e08
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 38 deletions.
53 changes: 53 additions & 0 deletions examples/cache.toit
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions examples/config.toit
Original file line number Diff line number Diff line change
@@ -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
70 changes: 70 additions & 0 deletions examples/ui.toit
Original file line number Diff line number Diff line change
@@ -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.info "This is an info message."
ui.warning "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."
59 changes: 47 additions & 12 deletions src/cli.toit
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,17 @@ 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.
Expand All @@ -40,6 +49,18 @@ interface Cli:
/** 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.
*/
Expand All @@ -61,7 +82,9 @@ class Cli_ implements Cli:
*/
ui/Ui

constructor .name --.ui:
constructor .name --.ui --cache/Cache? --config/Config?:
cache_ = cache
config_ = config

/** The cache object for this application. */
cache -> Cache:
Expand All @@ -73,6 +96,17 @@ class Cli_ implements Cli:
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.
Expand Down Expand Up @@ -236,21 +270,22 @@ class Command:
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.
*/
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 := Cli_ name --ui=ui
in the help output. By default it is active if no $cli is provided.
*/
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
parser.parse this arguments: | path/List parameters/Parameters |
invocation := Invocation.private_ app path parameters
invocation := Invocation.private_ cli path parameters
invocation.command.run-callback_.call invocation

add-ui-options_:
Expand Down
40 changes: 38 additions & 2 deletions src/ui.toit
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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.
Expand All @@ -225,6 +234,10 @@ class Ui:
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:
emit --kind=WARNING --structured=: "$object"
Expand Down Expand Up @@ -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]).
Expand All @@ -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.
Expand All @@ -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.
Expand Down
15 changes: 8 additions & 7 deletions tests/help_test.toit
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,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++:
Expand Down Expand Up @@ -69,8 +70,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"
Expand Down Expand Up @@ -117,8 +118,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")

Expand Down Expand Up @@ -147,8 +148,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 |
Expand Down
Loading

0 comments on commit 00e2e08

Please sign in to comment.