diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b20460c..0693cc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: - name: Setup constants shell: bash run: | - TOIT_VERSION=v2.0.0-alpha.24 + TOIT_VERSION=v2.0.0-alpha.100 echo "TOIT_VERSION=$TOIT_VERSION" >> $GITHUB_ENV export DOWNLOAD_DIR="${{ github.workspace }}/downloads" echo "DOWNLOAD_DIR=$DOWNLOAD_DIR" >> $GITHUB_ENV diff --git a/examples/main.toit b/examples/main.toit index 892d0f7..ebc75c8 100644 --- a/examples/main.toit +++ b/examples/main.toit @@ -67,71 +67,71 @@ Examples: main arguments: // Creates a root command. // The name of the root command is not used. - root_cmd := cli.Command "fleet_manager" - --long_help=""" + root-cmd := cli.Command "fleet_manager" + --long-help=""" This is an imaginary fleet manager for a fleet of Toit devices. It can not be used to manage the fleet, for example by adding or removing devices. """ - root_cmd.add create_status_command - root_cmd.add create_device_command + root-cmd.add create-status-command + root-cmd.add create-device-command - root_cmd.run arguments + root-cmd.run arguments // ============= Could be in a separate file status.toit. ============= -create_status_command -> cli.Command: +create-status-command -> cli.Command: return cli.Command "status" - --short_help="Shows the status of the fleet:" + --short-help="Shows the status of the fleet:" --options=[ - cli.Flag "verbose" --short_name="v" --short_help="Show more details." --multi, - cli.OptionInt "max-lines" --short_help="Maximum number of lines to show." --default=10, + cli.Flag "verbose" --short-name="v" --short-help="Show more details." --multi, + cli.OptionInt "max-lines" --short-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. + --global-priority=7, // Show this example for the root command. ] - --run=:: fleet_status it + --run=:: fleet-status it -fleet_status parsed/cli.Parsed: - verbose_list := parsed["verbose"] - trues := (verbose_list.filter: it).size - falses := verbose_list.size - trues - verbose_level := trues - falses - max_lines := parsed["max-lines"] +fleet-status parsed/cli.Parsed: + verbose-list := parsed["verbose"] + trues := (verbose-list.filter: it).size + falses := verbose-list.size - trues + verbose-level := trues - falses + max-lines := parsed["max-lines"] - print "Max $max_lines of status with verbosity-level $verbose_level." + print "Max $max-lines of status with verbosity-level $verbose-level." // ============= Could be in a separate file device.toit. ============= -create_device_command -> cli.Command: - device_cmd := cli.Command "device" +create-device-command -> cli.Command: + device-cmd := cli.Command "device" // Aliases can be used to invoke this command. --aliases=[ "dev", "thingy", ] - --long_help=""" + --long-help=""" Manage a particular device. Use the '--device' option to specify a specific device. Otherwise, the last used device is used. """ --options=[ - cli.Option "device" --short_name="d" - --short_help="The device to operate on." + cli.Option "device" --short-name="d" + --short-help="The device to operate on." ] - device_cmd.add create_reset_command - device_cmd.add create_upload_command - return device_cmd + device-cmd.add create-reset-command + device-cmd.add create-upload-command + return device-cmd -create_upload_command -> cli.Command: +create-upload-command -> cli.Command: return cli.Command "upload" - --long_help=""" + --long-help=""" Uploads the given file to the device. Other useful information here. @@ -139,50 +139,50 @@ create_upload_command -> cli.Command: --rest=[ cli.OptionString "data" --type="file" - --short_help="The data to upload." + --short-help="The data to upload." --required, ] --examples=[ cli.Example "Uploads the file 'foo.data' to the device 'foo':" --arguments="--device=foo foo.data" - --global_priority=8, // Include this example for super commands. + --global-priority=8, // Include this example for super commands. ] - --run=:: upload_to_device it + --run=:: upload-to-device it -upload_to_device parsed/cli.Parsed: +upload-to-device parsed/cli.Parsed: device := parsed["device"] data := parsed["data"] print "Uploading file '$data' to device '$device'." -create_reset_command -> cli.Command: +create-reset-command -> cli.Command: return cli.Command "reset" - --long_help=""" + --long-help=""" Resets the device. Other useful information here. """ --options=[ cli.OptionEnum "mode" ["hard", "soft"] - --short_help="The reset mode to use." - --short_name="m" + --short-help="The reset mode to use." + --short-name="m" --required, - cli.Flag "force" --short_name="f" - --short_help="Force the reset even if the device is active.", + cli.Flag "force" --short-name="f" + --short-help="Force the reset even if the device is active.", ] --examples=[ cli.Example "Do a soft-reset of device 'foo':" --arguments="--device=foo -m soft" - --global_priority=5, // Include this example for super commands. + --global-priority=5, // Include this example for super commands. cli.Example "Do a hard-reset:" --arguments="--mode=hard", ] - --run=:: reset_device it + --run=:: reset-device it -reset_device parsed/cli.Parsed: +reset-device parsed/cli.Parsed: device := parsed["device"] mode := parsed["mode"] force := parsed["force"] diff --git a/package.yaml b/package.yaml index 8769fa7..a07dda1 100644 --- a/package.yaml +++ b/package.yaml @@ -1,2 +1,5 @@ name: cli description: Tools, like an argument parser, to create command-line applications. + +environment: + sdk: ^2.0.0-alpha.100 diff --git a/src/cli.toit b/src/cli.toit index 6359eb5..0a508d4 100644 --- a/src/cli.toit +++ b/src/cli.toit @@ -4,7 +4,7 @@ import .parser_ import .utils_ -import .help_generator_ +import .help-generator_ /** When the arg-parser needs to report an error, or write a help message, it @@ -36,10 +36,10 @@ class Command: usage_/string? /** A short (one line) description of the command. */ - short_help_/string? + short-help_/string? /** A longer description of the command. */ - long_help_/string? + long-help_/string? /** Examples of the command. */ examples_/List @@ -54,7 +54,7 @@ class Command: rest_/List /** Whether this command should show up in the help. */ - is_hidden_/bool + is-hidden_/bool /** Subcommands. @@ -66,7 +66,7 @@ class Command: The function to invoke when this command is executed. May be null, in which case at least one subcommand must be specified. */ - run_callback_/Lambda? + run-callback_/Lambda? /** Constructs a new command. @@ -77,33 +77,33 @@ class Command: The $usage is usually constructed from the name and the arguments of the command, but can be provided explicitly if a different usage string is desired. - The $long_help is a longer description of the command that can span multiple lines. Use + The $long-help is a longer description of the command that can span multiple lines. Use indented lines to continue paragraphs (just like toitdoc). - The $short_help is a short description of the command. In most cases this help is a single + The $short-help is a short description of the command. In most cases this help is a single line, but it can span multiple lines/paragraphs if necessary. Use indented lines to continue paragraphs (just like toitdoc). */ - constructor .name --usage/string?=null --short_help/string?=null --long_help/string?=null --examples/List=[] \ + constructor .name --usage/string?=null --short-help/string?=null --long-help/string?=null --examples/List=[] \ --aliases/List=[] --options/List=[] --rest/List=[] --subcommands/List=[] --hidden/bool=false \ --run/Lambda?=null: usage_ = usage - short_help_ = short_help - long_help_ = long_help + short-help_ = short-help + long-help_ = long-help examples_ = examples aliases_ = aliases options_ = options rest_ = rest subcommands_ = subcommands - run_callback_ = run - is_hidden_ = hidden - if not subcommands.is_empty and not rest.is_empty: + run-callback_ = run + is-hidden_ = hidden + if not subcommands.is-empty and not rest.is-empty: throw "Cannot have both subcommands and rest arguments." - if run and not subcommands.is_empty: + if run and not subcommands.is-empty: throw "Cannot have both a run callback and subcommands." - hash_code -> int: - return name.hash_code + hash-code -> int: + return name.hash-code /** Adds a subcommand to this command. @@ -114,23 +114,23 @@ class Command: It is an error to add a subcommand to a command that has a run callback. */ add command/Command: - if not rest_.is_empty: + if not rest_.is-empty: throw "Cannot add subcommands to a command with rest arguments." - if run_callback_: + if run-callback_: throw "Cannot add subcommands to a command with a run callback." subcommands_.add command /** Returns the help string of this command. */ - help --invoked_command/string=program_name -> string: - generator := HelpGenerator [this] --invoked_command=invoked_command - generator.build_all - return generator.to_string + help --invoked-command/string=program-name -> string: + generator := HelpGenerator [this] --invoked-command=invoked-command + generator.build-all + return generator.to-string /** Returns the usage string of this command. */ - usage --invoked_command/string=program_name -> string: - generator := HelpGenerator [this] --invoked_command=invoked_command - generator.build_usage --as_section=false - return generator.to_string + usage --invoked-command/string=program-name -> string: + generator := HelpGenerator [this] --invoked-command=invoked-command + generator.build-usage --as-section=false + return generator.to-string /** Runs this command. @@ -138,76 +138,76 @@ class Command: Parses the given $arguments and then invokes the command or one of its subcommands with the $Parsed output. - The $invoked_command is used only for the usage message in case of an - error. It defaults to $program_name. + 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. */ - run arguments/List --invoked_command=program_name --ui/Ui=Ui_ -> none: - parser := Parser_ --ui=ui --invoked_command=invoked_command + run arguments/List --invoked-command=program-name --ui/Ui=Ui_ -> none: + parser := Parser_ --ui=ui --invoked-command=invoked-command parsed := parser.parse this arguments - parsed.command.run_callback_.call parsed + parsed.command.run-callback_.call parsed /** Checks this command and all subcommands for errors. */ - check --invoked_command=program_name: - check_ --path=[invoked_command] + check --invoked-command=program-name: + check_ --path=[invoked-command] - are_prefix_of_each_other_ str1/string str2/string -> bool: + are-prefix-of-each-other_ str1/string str2/string -> bool: m := min str1.size str2.size return str1[..m] == str2[..m] /** Checks this command and all subcommands. The $path, a list of strings, provides the sequence that was used to reach this command. - The $outer_long_options and $outer_short_options are the options that are + The $outer-long-options and $outer-short-options are the options that are available through supercommands. */ - check_ --path/List --outer_long_options/Set={} --outer_short_options/Set={}: + check_ --path/List --outer-long-options/Set={} --outer-short-options/Set={}: examples_.do: it as Example aliases_.do: it as string - long_options := {} - short_options := {} + long-options := {} + short-options := {} options_.do: | option/Option | - if long_options.contains option.name: + if long-options.contains option.name: throw "Ambiguous option of '$(path.join " ")': --$option.name." - if outer_long_options.contains option.name: + if outer-long-options.contains option.name: throw "Ambiguous option of '$(path.join " ")': --$option.name conflicts with global option." - long_options.add option.name + long-options.add option.name - if option.short_name: - if (short_options.any: are_prefix_of_each_other_ it option.short_name): - throw "Ambiguous option of '$(path.join " ")': -$option.short_name." - if (outer_short_options.any: are_prefix_of_each_other_ it option.short_name): - throw "Ambiguous option of '$(path.join " ")': -$option.short_name conflicts with global option." - short_options.add option.short_name + if option.short-name: + if (short-options.any: are-prefix-of-each-other_ it option.short-name): + throw "Ambiguous option of '$(path.join " ")': -$option.short-name." + if (outer-short-options.any: are-prefix-of-each-other_ it option.short-name): + throw "Ambiguous option of '$(path.join " ")': -$option.short-name conflicts with global option." + short-options.add option.short-name - have_seen_optional_rest := false + have-seen-optional-rest := false for i := 0; i < rest_.size; i++: option/Option := rest_[i] - if option.is_multi and not i == rest_.size - 1: + if option.is-multi and not i == rest_.size - 1: throw "Multi-option '$option.name' of '$(path.join " ")' must be the last rest argument." - if long_options.contains option.name: + if long-options.contains option.name: throw "Rest name '$option.name' of '$(path.join " ")' already used." - if outer_long_options.contains option.name: + if outer-long-options.contains option.name: throw "Rest name '$option.name' of '$(path.join " ")' already a global option." - if have_seen_optional_rest and option.is_required: + if have-seen-optional-rest and option.is-required: throw "Required rest argument '$option.name' of '$(path.join " ")' cannot follow optional rest argument." - if option.is_hidden: + if option.is-hidden: throw "Rest argument '$option.name' of '$(path.join " ")' cannot be hidden." - have_seen_optional_rest = not option.is_required - long_options.add option.name + have-seen-optional-rest = not option.is-required + long-options.add option.name - if not long_options.is_empty: + if not long-options.is-empty: // Make a copy first. - outer_long_options = outer_long_options.map: it - outer_long_options.add_all long_options - if not short_options.is_empty: + outer-long-options = outer-long-options.map: it + outer-long-options.add-all long-options + if not short-options.is-empty: // Make a copy first. - outer_short_options = outer_short_options.map: it - outer_short_options.add_all short_options + outer-short-options = outer-short-options.map: it + outer-short-options.add-all short-options subnames := {} subcommands_.do: | command/Command | @@ -218,16 +218,16 @@ class Command: subnames.add name command.check_ --path=(path + [command.name]) - --outer_long_options=outer_long_options - --outer_short_options=outer_short_options + --outer-long-options=outer-long-options + --outer-short-options=outer-short-options // We allow a command with a run callback if all subcommands are hidden. // As such, we could also allow commands without either. If desired, it should be // safe to remove the following check. - if subcommands_.is_empty and not run_callback_: + if subcommands_.is-empty and not run-callback_: throw "Command '$(path.join " ")' has no subcommands and no run callback." - find_subcommand_ name/string -> Command?: + find-subcommand_ name/string -> Command?: subcommands_.do: | command/Command | if command.name == name or command.aliases_.contains name: return command @@ -239,37 +239,37 @@ 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. -Non-rest options can be used with '--$name' or '-$short_name' (if provided). Rest options are positional +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. */ abstract class Option: name/string - short_name/string? - short_help/string? - is_required/bool - is_hidden/bool - is_multi/bool - should_split_commas/bool + short-name/string? + short-help/string? + is-required/bool + is-hidden/bool + is-multi/bool + should-split-commas/bool /** An alias for $OptionString. */ constructor name/string --default/string?=null --type/string="string" - --short_name/string?=null - --short_help/string?=null + --short-name/string?=null + --short-help/string?=null --required/bool=false --hidden/bool=false --multi/bool=false - --split_commas/bool=false: + --split-commas/bool=false: return OptionString name --default=default --type=type - --short_name=short_name - --short_help=short_help + --short-name=short-name + --short-help=short-help --required=required --hidden=hidden --multi=multi - --split_commas=split_commas + --split-commas=split-commas /** Creates an option with the given $name. @@ -283,9 +283,9 @@ abstract class Option: 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). - The $short_name is optional and will normally be a single-character string when provided. + The $short-name is optional and will normally be a single-character string when provided. - The $short_help is optional and is used for help output. It should be a full sentence, starting + The $short-help is optional and is used for help output. It should be a full sentence, starting with a capital letter and ending with a period. If $required is true, then the option must be provided. Otherwise, it is optional. @@ -296,24 +296,24 @@ abstract class Option: If $multi is true, then the option can be provided multiple times. The parsed value will be a list of strings. - If $split_commas is true, then $multi must be true too. Values given to this option are then + If $split-commas is true, then $multi must be true too. Values given to this option are then split on commas. For example, `--option a,b,c` will result in the list `["a", "b", "c"]`. */ - constructor.from_subclass .name --.short_name --.short_help --required --hidden --multi --split_commas: - name = to_kebab name - is_required = required - is_hidden = hidden - is_multi = multi - should_split_commas = split_commas - if name.contains "=" or name.starts_with "no-": throw "Invalid option name: $name" - if short_name and not is_alpha_num_string_ short_name: - throw "Invalid short option name: '$short_name'" - if split_commas and not multi: + constructor.from-subclass .name --.short-name --.short-help --required --hidden --multi --split-commas: + name = to-kebab name + is-required = required + is-hidden = hidden + is-multi = multi + should-split-commas = split-commas + if name.contains "=" or name.starts-with "no-": throw "Invalid option name: $name" + if short-name and not is-alpha-num-string_ short-name: + throw "Invalid short option name: '$short-name'" + if split-commas and not multi: throw "--split_commas is only valid for multi options." - if is_hidden and is_required: + if is-hidden and is-required: throw "Option can't be hidden and required." - static is_alpha_num_string_ str/string -> bool: + static is-alpha-num-string_ str/string -> bool: if str.size < 1: return false str.do --runes: | c | if not ('a' <= c <= 'z' or 'A' <= c <= 'Z' or '0' <= c <= '9'): @@ -326,9 +326,9 @@ abstract class Option: This output is used in the help output. */ - default_as_string -> string?: - default_value := default - if default_value != null: return default_value.stringify + default-as-string -> string?: + default-value := default + if default-value != null: return default-value.stringify return null /** The default value of this option. */ @@ -338,15 +338,15 @@ abstract class Option: abstract type -> string /** Whether this option is a flag. */ - abstract is_flag -> bool + abstract is-flag -> bool /** Parses the given $str and returns the parsed value. - If $for_help_example is true, only performs validation that is valid for examples. + If $for-help-example is true, only performs validation that is valid for examples. For example, a FileOption would not check that the file exists. */ - abstract parse str/string --for_help_example/bool=false -> any + abstract parse str/string --for-help-example/bool=false -> any /** @@ -369,21 +369,21 @@ class OptionString extends Option: constructor name/string --.default=null --.type="string" - --short_name/string?=null - --short_help/string?=null + --short-name/string?=null + --short-help/string?=null --required/bool=false --hidden/bool=false --multi/bool=false - --split_commas/bool=false: + --split-commas/bool=false: if multi and default: throw "Multi option can't have default value." if required and default: throw "Option can't have default value and be required." - super.from_subclass name --short_name=short_name --short_help=short_help \ + super.from-subclass name --short-name=short-name --short-help=short-help \ --required=required --hidden=hidden --multi=multi \ - --split_commas=split_commas + --split-commas=split-commas - is_flag: return false + is-flag: return false - parse str/string --for_help_example/bool=false -> string: + parse str/string --for-help-example/bool=false -> string: return str /** @@ -412,23 +412,23 @@ class OptionEnum extends Option: constructor name/string .values/List --.default=null --.type=(values.join "|") - --short_name/string?=null - --short_help/string?=null + --short-name/string?=null + --short-help/string?=null --required/bool=false --hidden/bool=false --multi/bool=false - --split_commas/bool=false: + --split-commas/bool=false: if multi and default: throw "Multi option can't have default value." if required and default: throw "Option can't have default value and be required." - super.from_subclass name --short_name=short_name --short_help=short_help \ + super.from-subclass name --short-name=short-name --short-help=short-help \ --required=required --hidden=hidden --multi=multi \ - --split_commas=split_commas + --split-commas=split-commas if default and not values.contains default: throw "Default value of '$name' is not a valid value: $default" - is_flag: return false + is-flag: return false - parse str/string --for_help_example/bool=false -> string: + parse str/string --for-help-example/bool=false -> string: if not values.contains str: throw "Invalid value for option '$name': '$str'. Valid values are: $(values.join ", ")." return str @@ -453,22 +453,22 @@ class OptionInt extends Option: constructor name/string --.default=null --.type="int" - --short_name/string?=null - --short_help/string?=null + --short-name/string?=null + --short-help/string?=null --required/bool=false --hidden/bool=false --multi/bool=false - --split_commas/bool=false: + --split-commas/bool=false: if multi and default: throw "Multi option can't have default value." if required and default: throw "Option can't have default value and be required." - super.from_subclass name --short_name=short_name --short_help=short_help \ + super.from-subclass name --short-name=short-name --short-help=short-help \ --required=required --hidden=hidden --multi=multi \ - --split_commas=split_commas + --split-commas=split-commas - is_flag: return false + is-flag: return false - parse str/string --for_help_example/bool=false -> int: - return int.parse str --on_error=: + parse str/string --for-help-example/bool=false -> int: + return int.parse str --on-error=: throw "Invalid integer value for option '$name': '$str'." /** @@ -493,22 +493,22 @@ class Flag extends Option: */ constructor name/string --.default=null - --short_name/string?=null - --short_help/string?=null \ + --short-name/string?=null + --short-help/string?=null \ --required/bool=false --hidden/bool=false --multi/bool=false: if multi and default != null: throw "Multi option can't have default value." if required and default != null: throw "Option can't have default value and be required." - super.from_subclass name --short_name=short_name --short_help=short_help \ - --required=required --hidden=hidden --multi=multi --no-split_commas + super.from-subclass name --short-name=short-name --short-help=short-help \ + --required=required --hidden=hidden --multi=multi --no-split-commas type -> string: return "true|false" - is_flag: return true + is-flag: return true - parse str/string --for_help_example/bool=false -> bool: + parse str/string --for-help-example/bool=false -> bool: if str == "true": return true if str == "false": return false throw "Invalid value for boolean flag '$name': '$str'. Valid values are: true, false." @@ -521,13 +521,13 @@ Examples are parsed and must be valid. They are used to generate the help. class Example: description/string arguments/string - global_priority/int + global-priority/int /** Creates an example. The $description should describe the example without any context. This is especially true - if the $global_priority is greater than 0 (see below). It should start with a capital + if the $global-priority is greater than 0 (see below). It should start with a capital letter and finish with a ":". It may contain newlines. Use indentation to group paragraphs (just like toitdoc). @@ -539,16 +539,16 @@ class Example: $arguments should be equal to `--gee`. If the example is for the command `bar`, then $arguments should be equal to `baz --gee`. - The $global_priority is used to sort the examples of sub commands. Examples with a higher priority + The $global-priority is used to sort the examples of sub commands. Examples with a higher priority are shown first. Examples with the same priority are sorted in the order in which they are encountered. - The $global_priority must be in the range 0 to 10 (both inclusive). The default value is 0. + The $global-priority must be in the range 0 to 10 (both inclusive). The default value is 0. - If the $global_priority is 0, then it is not used as example for super commands. + If the $global-priority is 0, then it is not used as example for super commands. */ - constructor .description --.arguments --.global_priority=0: - if not 0 <= global_priority <= 10: throw "INVALID_ARGUMENT" + constructor .description --.arguments --.global-priority=0: + if not 0 <= global-priority <= 10: throw "INVALID_ARGUMENT" /** The result of parsing the command line arguments. @@ -573,12 +573,12 @@ class Parsed: Contrary to the $options_ map, this set only contains options that were actually given, and not filled in by default values. */ - seen_options_/Set + seen-options_/Set /** Builds a new $Parsed object. */ - constructor.private_ .path .options_ .seen_options_: + constructor.private_ .path .options_ .seen-options_: /** The command that should be executed. @@ -593,22 +593,22 @@ class Parsed: to kebab-case. */ operator[] name/string -> any: - kebab_name := to_kebab name - return options_.get kebab_name --if_absent=: throw "No option named '$name'" + kebab-name := to-kebab name + return options_.get kebab-name --if-absent=: throw "No option named '$name'" /** Whether an option with the given $name was given on the command line. */ - was_provided name/string -> bool: - return seen_options_.contains name + was-provided name/string -> bool: + return seen-options_.contains name stringify: buffer := [] options_.do: | name value | buffer.add "$name=$value" return buffer.join " " -global_print_ str/string: print str +global-print_ str/string: print str class Ui_ implements Ui: - print str/string: global_print_ str + print str/string: global-print_ str abort: exit 1 diff --git a/src/help-generator_.toit b/src/help-generator_.toit new file mode 100644 index 0000000..a77864a --- /dev/null +++ b/src/help-generator_.toit @@ -0,0 +1,625 @@ +// Copyright (C) 2022 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 +import .parser_ +import .utils_ + +/** +The 'help' command that can be executed on the root command. + +It finds the selected command and prints its help. +*/ +help-command_ path/List arguments/List --invoked-command/string --ui/Ui: + // We are modifying the path, so make a copy. + path = path.copy + + command/Command := path.last + + for i := 0; i < arguments.size; i++: + argument := arguments[i] + if argument == "--": break + + // Simply drop all options. + if argument.starts-with "-": + continue + + subcommand := command.find-subcommand_ argument + if not subcommand: + ui.print "Unknown command: $argument" + ui.abort + unreachable + command = subcommand + path.add command + + print-help_ path --invoked-command=invoked-command --ui=ui + +/** +Prints the help for the given command. + +The command is identified by the $path where the command is the last element. +*/ +print-help_ path --invoked-command/string --ui/Ui: + generator := HelpGenerator path --invoked-command=invoked-command + generator.build-all + help := generator.to-string + ui.print help + +/** +Generates the help for a command. + +The class also serves as a string builder for the `build_X` methods. The methods call + the $write_, $writeln_, ... functions. The generated help can be obtained by + calling the $to-string method. +*/ +class HelpGenerator: + command_/Command + path_/List + invoked-command_/string + buffer_/List := [] // Buffered string. + // The index in the buffer of the last separator. + last-separator-pos_/int := 0 + + constructor .path_ --invoked-command/string: + invoked-command_ = invoked-command + command_=path_.last + + /** + Builds the full help for the command that was given to the constructor. + */ + build-all: + build-description + build-usage + build-aliases + build-commands + build-local-options + build-global-options + build-examples + + /** + Whether his help generator is for the root command. + */ + is-root-command_ -> bool: return path_.size == 1 + + /** + The options that are defined in super commands. + */ + global-options_ -> List: + result := [] + for i := 0; i < path_.size - 1; i++: + result.add-all path_[i].options_ + return result + + /** + Builds the description. + + If available, the description is the $Command.long-help_, otherwise, the + $Command.short-help_ is used. If none exists, no description is built. + */ + build-description -> none: + if help := command_.long-help_: + ensure-vertical-space_ + writeln_ (help.trim --right) + else if short-help := command_.short-help_: + ensure-vertical-space_ + writeln_ (short-help.trim --right) + + /** + Builds the usage section. + + If the command has a $Command.usage_ line, then that one is used. Otherwise the + usage line is built from the available options or subcommands. + + If $as-section is true, then the section is preceded by a "Usage:" title, indented, + and followed by an empty line. + */ + build-usage --as-section/bool=true -> none: + ensure-vertical-space_ + if as-section: + writeln_ "Usage:" + indentation := as-section ? 2 : 0 + if command_.usage_: + write_ command_.usage_ --indentation=indentation + if as-section: writeln_ + return + + // We want to construct a usage line like + // cmd --option1= --option2= [] [--] [...] + // We only show required named options in the usage line. + // Options are stored together with the (super)command that defines them. + // They are sorted by name. + // For the usage line we don't care for the short names of options. + + write_ invoked-command_ --indentation=indentation + has-more-options := false + for i := 0; i < path_.size; i++: + current-command/Command := path_[i] + if i != 0: write_ " $current-command.name" + current-options := current-command.options_ + sorted := current-options.sort: | a/Option b/Option | a.name.compare-to b.name + sorted.do: | option/Option | + if option.is-required: + write_ " --$option.name" + if not option.is-flag: + write_ "=<$option.type>" + else if not option.is-hidden: + has-more-options = true + + if not command_.subcommands_.is-empty: write_ " " + if has-more-options: write_ " []" + if not command_.rest_.is-empty: write_ " [--]" + command_.rest_.do: | option/Option | + type := option.type + option-str/string := ? + if type == "string": option-str = "<$option.name>" + else: option-str = "<$option.name:$option.type>" + if option.is-multi: option-str = "$option-str..." + if not option.is-required: option-str = "[$option-str]" + write_ " $option-str" + if as-section: writeln_ + + /** + Builds the aliases section. + + Only generates the alias section if the command is not the root command. + */ + build-aliases -> none: + if is-root-command_ or command_.aliases_.is-empty: return + ensure-vertical-space_ + writeln_ "Aliases:" + writeln_ (command_.aliases_.join ", ") --indentation=2 + + /** + Builds the commands section. + + Lists all subcommands with a short help. + Uses the first line of the long help if no short help is available. + + If the command is the root-command also adds the 'help' command. + */ + build-commands -> none: + if command_.subcommands_.is-empty: return + ensure-vertical-space_ + writeln_ "Commands:" + + commands-and-help := [] + has-help-subcommand := false + command_.subcommands_.do: | subcommand/Command | + if subcommand.name == "help": has-help-subcommand = true + subcommand.aliases_.do: if it == "help": has-help-subcommand = true + + if subcommand.is-hidden_: continue.do + + help-str := ? + if help := subcommand.short-help_: + help-str = help + else if long-help := subcommand.long-help_: + // Take the first paragraph (potentially multiple lines) of the long help. + paragraph-index := long-help.index-of "\n\n" + if paragraph-index == -1: + help-str = long-help + else: + help-str = long-help[..paragraph-index] + else: + help-str = "" + commands-and-help.add [subcommand.name, help-str] + + if not has-help-subcommand and is-root-command_: + commands-and-help.add ["help", "Show help for a command."] + + sorted-commands := commands-and-help.sort: | a/List b/List | a[0].compare-to b[0] + write-table_ sorted-commands --indentation=2 + + /** + Builds the local options section. + + Automatically adds the help option if it is not already defined. + */ + build-local-options -> none: + build-options_ --title="Options" command_.options_ --add-help + build-options_ --title="Rest" command_.rest_ --rest + + /** + Builds the global options section. + + Global options are the ones inherited from super commands. + */ + build-global-options -> none: + build-options_ --title="Global options" global-options_ + + build-options_ --title/string options/List --add-help/bool=false --rest/bool=false -> none: + if options.is-empty and not add-help: return + + ensure-vertical-space_ + writeln_ "$title:" + + if add-help: + has-help-flag := false + has-short-help-flag := false + options.do: | option/Option | + if option.name == "help": has-help-flag = true + if option.short-name == "h": has-short-help-flag = true + + if not has-help-flag: + options = options.copy + short-name := has-short-help-flag ? null : "h" + help-flag := Flag "help" --short-name=short-name --short-help="Show help for this command." + options.add help-flag + + sorted-options := options.sort: | a/Option b/Option | a.name.compare-to b.name + + max-short-name := 1 + sorted-options.do: + if it.short-name: max-short-name = max max-short-name it.short-name.size + + options-type-defaults-and-help := [] + + sorted-options.do: | option/Option | + if option.is-hidden: continue.do + option-str/string := ? + if rest: + option-str = "$option.name $option.type" + else: + if option.short-name: option-str = "-$option.short-name, " + else: option-str = " " + option-str = option-str.pad --right (3 + max-short-name) ' ' + option-str += "--$option.name" + type-str := option.type + if not option.is-flag: + option-str += " $type-str" + + help-str := option.short-help or "" + additional-info := "" + default-value := option.default + needs-separator := false + if default-value: + assert: not needs-separator + additional-info += "default: $default-value" + needs-separator = true + if option.is-multi: + if needs-separator: additional-info += ", " + additional-info += "multi" + needs-separator = true + if option.is-required: + if needs-separator: additional-info += ", " + additional-info += "required" + needs-separator = true + if additional-info != "": help-str += " ($additional-info)" + help-str = help-str.trim + + options-type-defaults-and-help.add [option-str, help-str] + + write-table_ options-type-defaults-and-help --indentation=2 + + /** + Builds the examples section. + + Local examples are first, followed by examples from subcommands, as long as + their $Example.global-priority is greater than 0. + + Examples from subcommands are sorted by their $Example.global-priority. + Examples that have the same priority are printed in the order in which they are + discovered. + + Each example is parsed and must be valid. The example's $Example.arguments are + just arguments to the command on which they are defined. This function is + prefixing the arguments with the commands. + + Options are moved to the command that defines them. + For short-hand options (like `-abc`) groups are moved to the first command that + accepts them all. + + # Examples + For commands `root --global= sub --local=` an example on the `sub` command + could be: `--global=xyz --local 123`. The example would be reconstructed as + `root --global=xyz sub --local 123`. Note, that options are only moved, but not canonicalized + to a certain way of writing options (like `--foo=bar` vs `--foo bar`). + */ + build-examples: + // Get the local examples directly, as we want to keep them on top, and as we + // don't want to filter them. + this-examples := command_.examples_.map: | example/Example | [example, path_] + sub-examples := [] + add-global-examples_ command_ sub-examples --path=path_ --skip-first-level + sub-examples.sort --in-place: | a/List b/List | a[0].global-priority - b[0].global-priority + + all-examples := this-examples + sub-examples + if all-examples.is-empty: return + + ensure-vertical-space_ + writeln_ "Examples:" + for i := 0; i < all-examples.size; i++: + if i != 0: writeln_ + example-and-path := all-examples[i] + example/Example := example-and-path[0] + example-path/List := example-and-path[1] + + build-example_ example --example-path=example-path + + /** + Adds all global examples (with $Example.global-priority > 0) to the $all-examples list. + + The $path provides the sequence that was used to reach this command. It is updated for + subcommands and stored together with the examples in $all-examples. + + If $skip-first-level is true, then does not add the examples of this command, but only + those of subcommands. + */ + add-global-examples_ command/Command all-examples/List --path/List --skip-first-level/bool=false -> none: + if not skip-first-level: + global-examples := (command.examples_.filter: it.global-priority > 0) + all-examples.add-all (global-examples.map: [it, path]) + + command.subcommands_.do: | subcommand/Command | + if subcommand.is-hidden_: continue.do + add-global-examples_ subcommand all-examples --path=(path + [subcommand]) + + /** + Builds a single example. + + The $example contains the description and arguments, and the $example-path is + the path to the command that contains the example. + */ + build-example_ example/Example --example-path/List: + description := example.description.trim --right + description-lines := ? + if description.contains "\n": description-lines = description.split "\n" + else: description-lines = [example.description] + description-lines.do: + write_ "# " --indentation=2 + writeln_ it + + arguments-strings := example.arguments.split "\n" + if arguments-strings.size > 1 and arguments-strings.last == "": + arguments-strings = arguments-strings[..arguments-strings.size - 1] + arguments-strings.do: + build-example-command-line_ it --example-path=example-path + + /** + Builds the example command line for the given $arguments-line. + The command that defined the example is identified by the $example-path. + */ + build-example-command-line_ arguments-line/string --example-path/List: + // Start by constructing a valid command line. + + // The prefix consists of the subcommands. + prefix := example-path[1..].map: | command/Command | command.name + // Split the arguments line into individual arguments. + // For example, `"foo --bar \"my password\""` is split into `["foo", "--bar", "my password"]`. + example-arguments := split-arguments_ arguments-line + command-line := prefix + example-arguments + + // 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 + + 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] + command-level[current-command] = j + current-command.options_.do: | option/Option | + if not parsed.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: + flags.add "--$option.name" + if option.short-name: flags.add "-$option.short-name" + + // Collect all the options that are destined for a (sub/super)command. + options-for-command := {:} // Map from command to list of options. + + argument-index := 0 + path-index := 0 + while argument-index < command-line.size: + argument := command-line[argument-index++] + if argument == "--": + break + if not argument.starts-with "-": + if path-index >= parsed-path.size - 1: + argument-index-- + break + else: + path-index++ + continue + + if argument.starts-with "--": + option-name := ? + equal-pos := argument.index-of "=" + if equal-pos >= 0: + option-name = argument[..equal-pos] + else if argument.starts-with "--no-": + option-name = "--$argument[5..]" + else: + option-name = argument + option-name = to-kebab option-name + option-command := option-to-command[option-name] + is-flag := flags.contains option-name + options-for-command.update option-command --init=(: []): | list/List | + list.add argument + if not is-flag and equal-pos < 0: + list.add command-line[argument-index++] + list + + else if argument.starts-with "-" and argument != "-": + highest-level := -1 + highest-command := null + takes-extra-arg := false + // We find the first command that accepts all of the options in this cluster. + for j := 1; j < argument.size; j++: + c := argument[j] + option-name := "-$(string.from-rune c)" + option-command := option-to-command[option-name] + level/int := command-level[option-command] + if level > highest-level: + highest-level = level + highest-command = option-command + if j == argument.size - 1 and not flags.contains option-name: + takes-extra-arg = true + + options-for-command.update highest-command --init=(: []): | list/List | + list.add argument + if takes-extra-arg: + list.add command-line[argument-index++] + list + + options-for-command.update parsed-path.last --init=(: []) : | list/List | + list.add-all command-line[argument-index..] + list + + // Reconstruct the full command line, but now with the options next to the + // commands that defined them. + full-command := [] + parsed-path.do: | current-command | + full-command.add current-command.name + command-options := options-for-command.get current-command + if command-options: + command-options.do: | option/string | + full-command.add option + + writeln_ (full-command.join " ") --indentation=2 + + /** + Splits a string into individual arguments. + */ + split-arguments_ arguments-string/string -> List: + arguments-string = arguments-string.trim + arguments-string += " " + arguments := [] + // Currently only handles double quotes. + in-quotes := false + start := 0 + for i := 0; i < arguments-string.size; i++: + c := arguments-string[i] + if c == ' ' and not in-quotes: + if i != start: + arguments.add arguments-string[start..i] + start = i + 1 + else if c == '"': + in-quotes = not in-quotes + + if in-quotes: throw "Unterminated quotes: $arguments-string.trim" + return arguments + + write_ str/string: + buffer_.add str + + write_ str/string --indentation/int --indent-first-line/bool=true: + indentation-str := " " * indentation + if indent-first-line: + buffer_.add indentation-str + buffer_.add (str.replace "\n" "\n$indentation-str") + + writeln_ str/string="": + if str != "": buffer_.add str + buffer_.add "\n" + + writeln_ str/string --indentation/int --indent-first-line/bool=true: + write_ str --indentation=indentation --indent-first-line=indent-first-line + buffer_.add "\n" + + count-occurrences_ str/string needle/string -> int: + if needle.size == 0: throw "INVALID_ARGUMENT" + count := 0 + index := 0 + while true: + index = str.index-of needle index + if index >= 0: + count++ + index += needle.size + else: + return count + + /** + Writes a table into the buffer. + + Determines the size of each column, and aligns the columns. + Uses 2 spaces as the column separator. + */ + write-table_ rows/List --indentation/int=0: + if rows.is-empty: return + column-count := rows[0].size + + // If an entry in the row has multiple lines split it into multiple rows. + split-rows := [] + rows.do: | row/List | + max-line-count := 1 + row.do: | entry/string | + // Remove trailing whitespace. + trimmed := entry.trim --right + // Count number of lines in the entry. + line-count := 1 + (count-occurrences_ trimmed "\n") + max-line-count = max max-line-count line-count + + if max-line-count == 1: + split-rows.add row + else: + columns := [] + row.do: | entry/string | + // Remove trailing whitespace. + trimmed := entry.trim --right + // Split the entry into lines. + lines := trimmed.split "\n" + (max-line-count - lines.size).repeat: + lines.add "" + columns.add lines + max-line-count.repeat: | line-index/int | + line := columns.map: it[line-index] + split-rows.add line + + max-len := List column-count: 0 + split-rows.do: | row/List | + for i := 0; i < column-count; i++: + max-len[i] = max max-len[i] row[i].size + + split-rows.do: | row/List | + write_ --indentation=indentation "" + for i := 0; i < column-count; i++: + entry := row[i] + write_ entry + needs-spacing := false + for j := i + 1; j < column-count; j++: + if row[j] != "": + needs-spacing = true + break + if needs-spacing: + write_ " " * (max-len[i] - entry.size + 2) + writeln_ + + /** + Ensures that there is vertical space at the current position. + + Vertical space is currently just an empty line. + */ + ensure-vertical-space_ -> none: + if buffer_.size == last-separator-pos_: + // Nothing was written since the last separator. + // There is still a separator at the end of the buffer. + return + writeln_ + last-separator-pos_ = buffer_.size + + to-string -> string: + return buffer_.join "" + + +global-print_ str/string: + print str + +class ExampleUi_ implements Ui: + example_/string + + constructor .example_: + + print str/string: + global-print_ str + + abort: + throw "Error in example: $example_" diff --git a/src/help_generator_.toit b/src/help_generator_.toit deleted file mode 100644 index 61c9051..0000000 --- a/src/help_generator_.toit +++ /dev/null @@ -1,625 +0,0 @@ -// Copyright (C) 2022 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 -import .parser_ -import .utils_ - -/** -The 'help' command that can be executed on the root command. - -It finds the selected command and prints its help. -*/ -help_command_ path/List arguments/List --invoked_command/string --ui/Ui: - // We are modifying the path, so make a copy. - path = path.copy - - command/Command := path.last - - for i := 0; i < arguments.size; i++: - argument := arguments[i] - if argument == "--": break - - // Simply drop all options. - if argument.starts_with "-": - continue - - subcommand := command.find_subcommand_ argument - if not subcommand: - ui.print "Unknown command: $argument" - ui.abort - unreachable - command = subcommand - path.add command - - print_help_ path --invoked_command=invoked_command --ui=ui - -/** -Prints the help for the given command. - -The command is identified by the $path where the command is the last element. -*/ -print_help_ path --invoked_command/string --ui/Ui: - generator := HelpGenerator path --invoked_command=invoked_command - generator.build_all - help := generator.to_string - ui.print help - -/** -Generates the help for a command. - -The class also serves as a string builder for the `build_X` methods. The methods call - the $write_, $writeln_, ... functions. The generated help can be obtained by - calling the $to_string method. -*/ -class HelpGenerator: - command_/Command - path_/List - invoked_command_/string - buffer_/List := [] // Buffered string. - // The index in the buffer of the last separator. - last_separator_pos_/int := 0 - - constructor .path_ --invoked_command/string: - invoked_command_ = invoked_command - command_=path_.last - - /** - Builds the full help for the command that was given to the constructor. - */ - build_all: - build_description - build_usage - build_aliases - build_commands - build_local_options - build_global_options - build_examples - - /** - Whether his help generator is for the root command. - */ - is_root_command_ -> bool: return path_.size == 1 - - /** - The options that are defined in super commands. - */ - global_options_ -> List: - result := [] - for i := 0; i < path_.size - 1; i++: - result.add_all path_[i].options_ - return result - - /** - Builds the description. - - If available, the description is the $Command.long_help_, otherwise, the - $Command.short_help_ is used. If none exists, no description is built. - */ - build_description -> none: - if help := command_.long_help_: - ensure_vertical_space_ - writeln_ (help.trim --right) - else if short_help := command_.short_help_: - ensure_vertical_space_ - writeln_ (short_help.trim --right) - - /** - Builds the usage section. - - If the command has a $Command.usage_ line, then that one is used. Otherwise the - usage line is built from the available options or subcommands. - - If $as_section is true, then the section is preceded by a "Usage:" title, indented, - and followed by an empty line. - */ - build_usage --as_section/bool=true -> none: - ensure_vertical_space_ - if as_section: - writeln_ "Usage:" - indentation := as_section ? 2 : 0 - if command_.usage_: - write_ command_.usage_ --indentation=indentation - if as_section: writeln_ - return - - // We want to construct a usage line like - // cmd --option1= --option2= [] [--] [...] - // We only show required named options in the usage line. - // Options are stored together with the (super)command that defines them. - // They are sorted by name. - // For the usage line we don't care for the short names of options. - - write_ invoked_command_ --indentation=indentation - has_more_options := false - for i := 0; i < path_.size; i++: - current_command/Command := path_[i] - if i != 0: write_ " $current_command.name" - current_options := current_command.options_ - sorted := current_options.sort: | a/Option b/Option | a.name.compare_to b.name - sorted.do: | option/Option | - if option.is_required: - write_ " --$option.name" - if not option.is_flag: - write_ "=<$option.type>" - else if not option.is_hidden: - has_more_options = true - - if not command_.subcommands_.is_empty: write_ " " - if has_more_options: write_ " []" - if not command_.rest_.is_empty: write_ " [--]" - command_.rest_.do: | option/Option | - type := option.type - option_str/string := ? - if type == "string": option_str = "<$option.name>" - else: option_str = "<$option.name:$option.type>" - if option.is_multi: option_str = "$option_str..." - if not option.is_required: option_str = "[$option_str]" - write_ " $option_str" - if as_section: writeln_ - - /** - Builds the aliases section. - - Only generates the alias section if the command is not the root command. - */ - build_aliases -> none: - if is_root_command_ or command_.aliases_.is_empty: return - ensure_vertical_space_ - writeln_ "Aliases:" - writeln_ (command_.aliases_.join ", ") --indentation=2 - - /** - Builds the commands section. - - Lists all subcommands with a short help. - Uses the first line of the long help if no short help is available. - - If the command is the root-command also adds the 'help' command. - */ - build_commands -> none: - if command_.subcommands_.is_empty: return - ensure_vertical_space_ - writeln_ "Commands:" - - commands_and_help := [] - has_help_subcommand := false - command_.subcommands_.do: | subcommand/Command | - if subcommand.name == "help": has_help_subcommand = true - subcommand.aliases_.do: if it == "help": has_help_subcommand = true - - if subcommand.is_hidden_: continue.do - - help_str := ? - if help := subcommand.short_help_: - help_str = help - else if long_help := subcommand.long_help_: - // Take the first paragraph (potentially multiple lines) of the long help. - paragraph_index := long_help.index_of "\n\n" - if paragraph_index == -1: - help_str = long_help - else: - help_str = long_help[..paragraph_index] - else: - help_str = "" - commands_and_help.add [subcommand.name, help_str] - - if not has_help_subcommand and is_root_command_: - commands_and_help.add ["help", "Show help for a command."] - - sorted_commands := commands_and_help.sort: | a/List b/List | a[0].compare_to b[0] - write_table_ sorted_commands --indentation=2 - - /** - Builds the local options section. - - Automatically adds the help option if it is not already defined. - */ - build_local_options -> none: - build_options_ --title="Options" command_.options_ --add_help - build_options_ --title="Rest" command_.rest_ --rest - - /** - Builds the global options section. - - Global options are the ones inherited from super commands. - */ - build_global_options -> none: - build_options_ --title="Global options" global_options_ - - build_options_ --title/string options/List --add_help/bool=false --rest/bool=false -> none: - if options.is_empty and not add_help: return - - ensure_vertical_space_ - writeln_ "$title:" - - if add_help: - has_help_flag := false - has_short_help_flag := false - options.do: | option/Option | - if option.name == "help": has_help_flag = true - if option.short_name == "h": has_short_help_flag = true - - if not has_help_flag: - options = options.copy - short_name := has_short_help_flag ? null : "h" - help_flag := Flag "help" --short_name=short_name --short_help="Show help for this command." - options.add help_flag - - sorted_options := options.sort: | a/Option b/Option | a.name.compare_to b.name - - max_short_name := 1 - sorted_options.do: - if it.short_name: max_short_name = max max_short_name it.short_name.size - - options_type_defaults_and_help := [] - - sorted_options.do: | option/Option | - if option.is_hidden: continue.do - option_str/string := ? - if rest: - option_str = "$option.name $option.type" - else: - if option.short_name: option_str = "-$option.short_name, " - else: option_str = " " - option_str = option_str.pad --right (3 + max_short_name) ' ' - option_str += "--$option.name" - type_str := option.type - if not option.is_flag: - option_str += " $type_str" - - help_str := option.short_help or "" - additional_info := "" - default_value := option.default - needs_separator := false - if default_value: - assert: not needs_separator - additional_info += "default: $default_value" - needs_separator = true - if option.is_multi: - if needs_separator: additional_info += ", " - additional_info += "multi" - needs_separator = true - if option.is_required: - if needs_separator: additional_info += ", " - additional_info += "required" - needs_separator = true - if additional_info != "": help_str += " ($additional_info)" - help_str = help_str.trim - - options_type_defaults_and_help.add [option_str, help_str] - - write_table_ options_type_defaults_and_help --indentation=2 - - /** - Builds the examples section. - - Local examples are first, followed by examples from subcommands, as long as - their $Example.global_priority is greater than 0. - - Examples from subcommands are sorted by their $Example.global_priority. - Examples that have the same priority are printed in the order in which they are - discovered. - - Each example is parsed and must be valid. The example's $Example.arguments are - just arguments to the command on which they are defined. This function is - prefixing the arguments with the commands. - - Options are moved to the command that defines them. - For short-hand options (like `-abc`) groups are moved to the first command that - accepts them all. - - # Examples - For commands `root --global= sub --local=` an example on the `sub` command - could be: `--global=xyz --local 123`. The example would be reconstructed as - `root --global=xyz sub --local 123`. Note, that options are only moved, but not canonicalized - to a certain way of writing options (like `--foo=bar` vs `--foo bar`). - */ - build_examples: - // Get the local examples directly, as we want to keep them on top, and as we - // don't want to filter them. - this_examples := command_.examples_.map: | example/Example | [example, path_] - sub_examples := [] - add_global_examples_ command_ sub_examples --path=path_ --skip_first_level - sub_examples.sort --in_place: | a/List b/List | a[0].global_priority - b[0].global_priority - - all_examples := this_examples + sub_examples - if all_examples.is_empty: return - - ensure_vertical_space_ - writeln_ "Examples:" - for i := 0; i < all_examples.size; i++: - if i != 0: writeln_ - example_and_path := all_examples[i] - example/Example := example_and_path[0] - example_path/List := example_and_path[1] - - build_example_ example --example_path=example_path - - /** - Adds all global examples (with $Example.global_priority > 0) to the $all_examples list. - - The $path provides the sequence that was used to reach this command. It is updated for - subcommands and stored together with the examples in $all_examples. - - If $skip_first_level is true, then does not add the examples of this command, but only - those of subcommands. - */ - add_global_examples_ command/Command all_examples/List --path/List --skip_first_level/bool=false -> none: - if not skip_first_level: - global_examples := (command.examples_.filter: it.global_priority > 0) - all_examples.add_all (global_examples.map: [it, path]) - - command.subcommands_.do: | subcommand/Command | - if subcommand.is_hidden_: continue.do - add_global_examples_ subcommand all_examples --path=(path + [subcommand]) - - /** - Builds a single example. - - The $example contains the description and arguments, and the $example_path is - the path to the command that contains the example. - */ - build_example_ example/Example --example_path/List: - description := example.description.trim --right - description_lines := ? - if description.contains "\n": description_lines = description.split "\n" - else: description_lines = [example.description] - description_lines.do: - write_ "# " --indentation=2 - writeln_ it - - arguments_strings := example.arguments.split "\n" - if arguments_strings.size > 1 and arguments_strings.last == "": - arguments_strings = arguments_strings[..arguments_strings.size - 1] - arguments_strings.do: - build_example_command_line_ it --example_path=example_path - - /** - Builds the example command line for the given $arguments_line. - The command that defined the example is identified by the $example_path. - */ - build_example_command_line_ arguments_line/string --example_path/List: - // Start by constructing a valid command line. - - // The prefix consists of the subcommands. - prefix := example_path[1..].map: | command/Command | command.name - // Split the arguments line into individual arguments. - // For example, `"foo --bar \"my password\""` is split into `["foo", "--bar", "my password"]`. - example_arguments := split_arguments_ arguments_line - command_line := prefix + example_arguments - - // 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 - - 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] - command_level[current_command] = j - current_command.options_.do: | option/Option | - if not parsed.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: - flags.add "--$option.name" - if option.short_name: flags.add "-$option.short_name" - - // Collect all the options that are destined for a (sub/super)command. - options_for_command := {:} // Map from command to list of options. - - argument_index := 0 - path_index := 0 - while argument_index < command_line.size: - argument := command_line[argument_index++] - if argument == "--": - break - if not argument.starts_with "-": - if path_index >= parsed_path.size - 1: - argument_index-- - break - else: - path_index++ - continue - - if argument.starts_with "--": - option_name := ? - equal_pos := argument.index_of "=" - if equal_pos >= 0: - option_name = argument[..equal_pos] - else if argument.starts_with "--no-": - option_name = "--$argument[5..]" - else: - option_name = argument - option_name = to_kebab option_name - option_command := option_to_command[option_name] - is_flag := flags.contains option_name - options_for_command.update option_command --init=(: []): | list/List | - list.add argument - if not is_flag and equal_pos < 0: - list.add command_line[argument_index++] - list - - else if argument.starts_with "-" and argument != "-": - highest_level := -1 - highest_command := null - takes_extra_arg := false - // We find the first command that accepts all of the options in this cluster. - for j := 1; j < argument.size; j++: - c := argument[j] - option_name := "-$(string.from_rune c)" - option_command := option_to_command[option_name] - level/int := command_level[option_command] - if level > highest_level: - highest_level = level - highest_command = option_command - if j == argument.size - 1 and not flags.contains option_name: - takes_extra_arg = true - - options_for_command.update highest_command --init=(: []): | list/List | - list.add argument - if takes_extra_arg: - list.add command_line[argument_index++] - list - - options_for_command.update parsed_path.last --init=(: []) : | list/List | - list.add_all command_line[argument_index..] - list - - // Reconstruct the full command line, but now with the options next to the - // commands that defined them. - full_command := [] - parsed_path.do: | current_command | - full_command.add current_command.name - command_options := options_for_command.get current_command - if command_options: - command_options.do: | option/string | - full_command.add option - - writeln_ (full_command.join " ") --indentation=2 - - /** - Splits a string into individual arguments. - */ - split_arguments_ arguments_string/string -> List: - arguments_string = arguments_string.trim - arguments_string += " " - arguments := [] - // Currently only handles double quotes. - in_quotes := false - start := 0 - for i := 0; i < arguments_string.size; i++: - c := arguments_string[i] - if c == ' ' and not in_quotes: - if i != start: - arguments.add arguments_string[start..i] - start = i + 1 - else if c == '"': - in_quotes = not in_quotes - - if in_quotes: throw "Unterminated quotes: $arguments_string.trim" - return arguments - - write_ str/string: - buffer_.add str - - write_ str/string --indentation/int --indent_first_line/bool=true: - indentation_str := " " * indentation - if indent_first_line: - buffer_.add indentation_str - buffer_.add (str.replace "\n" "\n$indentation_str") - - writeln_ str/string="": - if str != "": buffer_.add str - buffer_.add "\n" - - writeln_ str/string --indentation/int --indent_first_line/bool=true: - write_ str --indentation=indentation --indent_first_line=indent_first_line - buffer_.add "\n" - - count_occurrences_ str/string needle/string -> int: - if needle.size == 0: throw "INVALID_ARGUMENT" - count := 0 - index := 0 - while true: - index = str.index_of needle index - if index >= 0: - count++ - index += needle.size - else: - return count - - /** - Writes a table into the buffer. - - Determines the size of each column, and aligns the columns. - Uses 2 spaces as the column separator. - */ - write_table_ rows/List --indentation/int=0: - if rows.is_empty: return - column_count := rows[0].size - - // If an entry in the row has multiple lines split it into multiple rows. - split_rows := [] - rows.do: | row/List | - max_line_count := 1 - row.do: | entry/string | - // Remove trailing whitespace. - trimmed := entry.trim --right - // Count number of lines in the entry. - line_count := 1 + (count_occurrences_ trimmed "\n") - max_line_count = max max_line_count line_count - - if max_line_count == 1: - split_rows.add row - else: - columns := [] - row.do: | entry/string | - // Remove trailing whitespace. - trimmed := entry.trim --right - // Split the entry into lines. - lines := trimmed.split "\n" - (max_line_count - lines.size).repeat: - lines.add "" - columns.add lines - max_line_count.repeat: | line_index/int | - line := columns.map: it[line_index] - split_rows.add line - - max_len := List column_count: 0 - split_rows.do: | row/List | - for i := 0; i < column_count; i++: - max_len[i] = max max_len[i] row[i].size - - split_rows.do: | row/List | - write_ --indentation=indentation "" - for i := 0; i < column_count; i++: - entry := row[i] - write_ entry - needs_spacing := false - for j := i + 1; j < column_count; j++: - if row[j] != "": - needs_spacing = true - break - if needs_spacing: - write_ " " * (max_len[i] - entry.size + 2) - writeln_ - - /** - Ensures that there is vertical space at the current position. - - Vertical space is currently just an empty line. - */ - ensure_vertical_space_ -> none: - if buffer_.size == last_separator_pos_: - // Nothing was written since the last separator. - // There is still a separator at the end of the buffer. - return - writeln_ - last_separator_pos_ = buffer_.size - - to_string -> string: - return buffer_.join "" - - -global_print_ str/string: - print str - -class ExampleUi_ implements Ui: - example_/string - - constructor .example_: - - print str/string: - global_print_ str - - abort: - throw "Error in example: $example_" diff --git a/src/parser_.toit b/src/parser_.toit index 5b1692a..d103ba8 100644 --- a/src/parser_.toit +++ b/src/parser_.toit @@ -3,71 +3,71 @@ // found in the package's LICENSE file. import .cli -import .help_generator_ +import .help-generator_ import .utils_ class Parser_: ui_/Ui - invoked_command_/string - usage_on_error_/bool + invoked-command_/string + usage-on-error_/bool - constructor --ui/Ui --invoked_command/string --usage_on_error=true: + constructor --ui/Ui --invoked-command/string --usage-on-error=true: ui_ = ui - invoked_command_ = invoked_command - usage_on_error_ = usage_on_error + invoked-command_ = invoked-command + usage-on-error_ = usage-on-error fatal path/List str/string: ui_.print "Error: $str" - if usage_on_error_: + if usage-on-error_: ui_.print "" - help_command_ path [] --invoked_command=invoked_command_ --ui=ui_ + 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 --for-help-example/bool=false -> Parsed: path := [] // Populate the options from the default values or empty lists (for multi-options) options := {:} - seen_options := {} - all_named_options := {:} - all_short_options := {:} + seen-options := {} + all-named-options := {:} + all-short-options := {:} - 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 - options[option.name].add_all parsed - else if seen_options.contains option.name: + 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 + 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 + seen-options.add option.name - create_help := : | arguments/List | - help_command := Command "help" --run=:: - help_command_ path arguments --invoked_command=invoked_command_ --ui=ui_ - Parsed.private_ [help_command] {:} {} + create-help := : | arguments/List | + help-command := Command "help" --run=:: + help-command_ path arguments --invoked-command=invoked-command_ --ui=ui_ + Parsed.private_ [help-command] {:} {} command/Command? := null - set_command := : | new_command/Command | - new_command.options_.do: | option/Option | - all_named_options[option.name] = option - if option.short_name: all_short_options[option.short_name] = option + set-command := : | new-command/Command | + new-command.options_.do: | option/Option | + all-named-options[option.name] = option + if option.short-name: all-short-options[option.short-name] = option // The rest options are only allowed for the last command. - (new_command.options_ + new_command.rest_).do: | option/Option | - if option.is_multi: + (new-command.options_ + new-command.rest_).do: | option/Option | + if option.is-multi: options[option.name] = [] else: options[option.name] = option.default - command = new_command + command = new-command path.add command - set_command.call root_command + set-command.call root-command rest := [] @@ -75,106 +75,106 @@ class Parser_: while index < arguments.size: argument := arguments[index++] if argument == "--": - rest.add_all arguments[index ..] + rest.add-all arguments[index ..] break // We're done! - if argument.starts_with "--": + if argument.starts-with "--": value := null // Get the option name. - split := argument.index_of "=" + split := argument.index-of "=" name := (split < 0) ? argument[2..] : argument[2..split] if split >= 0: value = argument[split + 1 ..] - is_inverted := false - if name.starts_with "no-": - is_inverted = true + is-inverted := false + if name.starts-with "no-": + is-inverted = true name = name[3..] - kebab_name := to_kebab name + kebab-name := to-kebab name - option := all_named_options.get kebab_name + 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 create-help.call [] fatal path "Unknown option: --$name" - if option.is_flag and value != null: + if option.is-flag and value != null: fatal path "Cannot specify value for boolean flag --$name." - if option.is_flag: - value = is_inverted ? "false" : "true" - else if is_inverted: + if option.is-flag: + value = is-inverted ? "false" : "true" + else if is-inverted: fatal path "Cannot invert non-boolean flag --$name." if value == null: if index >= arguments.size: fatal path "Option --$name requires an argument." value = arguments[index++] - add_option.call option value + add-option.call option value - else if argument.starts_with "-": + else if argument.starts-with "-": // Compute the option and the effective name. We allow short form prefixes to have // the value encoded in the same argument like -s"123 + 345", so we have to search // for prefixes. for i := 1; i < argument.size; : - option_length := 1 - short_name := null + option-length := 1 + short-name := null option := null - while i + option_length <= argument.size: - short_name = argument[i..i + option_length] - option = all_short_options.get short_name + while i + option-length <= argument.size: + short-name = argument[i..i + option-length] + option = all-short-options.get short-name if option: break - option_length++ + option-length++ if not option: - if short_name == "h": return create_help.call [] - fatal path "Unknown option: -$short_name" + if short-name == "h": return create-help.call [] + fatal path "Unknown option: -$short-name" - i += option_length + i += option-length if option is Flag: - add_option.call option "true" + add-option.call option "true" else: if i < argument.size: - add_option.call option argument[i ..] + add-option.call option argument[i ..] break else: if index >= arguments.size: - fatal path "Option -$short_name requires an argument." - add_option.call option arguments[index++] + fatal path "Option -$short-name requires an argument." + add-option.call option arguments[index++] break - else if not command.run_callback_: - subcommand := command.find_subcommand_ argument + else if not command.run-callback_: + subcommand := command.find-subcommand_ argument if not subcommand: - if argument == "help" and command == root_command: + if argument == "help" and command == root-command: // Special case for the help command. - return create_help.call arguments[index..] + return create-help.call arguments[index..] fatal path "Unknown command: $argument" - set_command.call subcommand + set-command.call subcommand else: rest.add argument - all_named_options.do: | name/string option/Option | - if option.is_required and not seen_options.contains name: + all-named-options.do: | name/string option/Option | + if option.is-required and not seen-options.contains name: fatal path "Required option $name is missing." - rest_index := 0 - command.rest_.do: | rest_option/Option | - if rest_option.is_required and rest_index >= rest.size: - fatal path "Missing required rest argument: '$rest_option.name'." - if rest_index >= rest.size: continue.do + rest-index := 0 + command.rest_.do: | rest-option/Option | + if rest-option.is-required and rest-index >= rest.size: + fatal path "Missing required rest argument: '$rest-option.name'." + if rest-index >= rest.size: continue.do - if rest_option.is_multi: - while rest_index < rest.size: - add_option.call rest_option rest[rest_index++] + if rest-option.is-multi: + while rest-index < rest.size: + add-option.call rest-option rest[rest-index++] else: - add_option.call rest_option rest[rest_index++] + add-option.call rest-option rest[rest-index++] - if rest_index < rest.size: - fatal path "Unexpected rest argument: '$rest[rest_index]'." + if rest-index < rest.size: + fatal path "Unexpected rest argument: '$rest[rest-index]'." - if not command.run_callback_: + if not command.run-callback_: fatal path "Missing subcommand." - return Parsed.private_ path options seen_options + return Parsed.private_ path options seen-options diff --git a/src/utils_.toit b/src/utils_.toit index f7f26c9..8669504 100644 --- a/src/utils_.toit +++ b/src/utils_.toit @@ -7,5 +7,5 @@ Converts snake-case strings to kebab case. For example, "hello_world" becomes "hello-world". */ -to_kebab str/string -> string: +to-kebab str/string -> string: return str.replace --all "_" "-" diff --git a/tests/check_test.toit b/tests/check_test.toit index af02cd0..57e90a7 100644 --- a/tests/check_test.toit +++ b/tests/check_test.toit @@ -6,130 +6,130 @@ import cli import expect show * main: - missing_run - ambiguous_option - ambiguous_command - rest_and_command - run_and_command + missing-run + ambiguous-option + ambiguous-command + rest-and-command + run-and-command rest - hidden_rest - snake_kebab + hidden-rest + snake-kebab -missing_run: +missing-run: root := cli.Command "root" - expect_throw "Command 'root' has no subcommands and no run callback.": root.check --invoked_command="root" + expect-throw "Command 'root' has no subcommands and no run callback.": root.check --invoked-command="root" sub := cli.Command "sub" root.add sub - expect_throw "Command 'root sub' has no subcommands and no run callback.": root.check --invoked_command="root" + expect-throw "Command 'root sub' has no subcommands and no run callback.": root.check --invoked-command="root" subsub1 := cli.Command "subsub1" --run=(:: null) sub.add subsub1 subsub2 := cli.Command "subsub2" sub.add subsub2 - expect_throw "Command 'root sub subsub2' has no subcommands and no run callback.": root.check --invoked_command="root" + expect-throw "Command 'root sub subsub2' has no subcommands and no run callback.": root.check --invoked-command="root" // Note that hidden subcommands are fine, though. root = cli.Command "root" - sub_hidden := cli.Command "sub" --hidden --run=(:: null) - root.add sub_hidden + sub-hidden := cli.Command "sub" --hidden --run=(:: null) + root.add sub-hidden root.run ["sub"] -ambiguous_option: +ambiguous-option: root := cli.Command "root" --options=[ - cli.Option "foo" --short_name="a", - cli.Option "foo" --short_name="b", + cli.Option "foo" --short-name="a", + cli.Option "foo" --short-name="b", ] --run=(:: null) - expect_throw "Ambiguous option of 'root': --foo.": root.check --invoked_command="root" + expect-throw "Ambiguous option of 'root': --foo.": root.check --invoked-command="root" root = cli.Command "root" --options=[ - cli.Option "foo" --short_name="a", - cli.Option "bar" --short_name="a", + cli.Option "foo" --short-name="a", + cli.Option "bar" --short-name="a", ] --run=(:: null) - expect_throw "Ambiguous option of 'root': -a.": root.check --invoked_command="root" + expect-throw "Ambiguous option of 'root': -a.": root.check --invoked-command="root" root = cli.Command "root" --options=[ - cli.Option "foo" --short_name="a", - cli.Option "bar" --short_name="ab", + cli.Option "foo" --short-name="a", + cli.Option "bar" --short-name="ab", ] --run=(:: null) - expect_throw "Ambiguous option of 'root': -ab.": root.check --invoked_command="root" + expect-throw "Ambiguous option of 'root': -ab.": root.check --invoked-command="root" root = cli.Command "root" --options=[ - cli.Option "foo" --short_name="a", + cli.Option "foo" --short-name="a", ] sub := cli.Command "sub" --options=[ - cli.Option "foo" --short_name="b", + cli.Option "foo" --short-name="b", ] --run=(:: null) root.add sub - expect_throw "Ambiguous option of 'root sub': --foo conflicts with global option.": - root.check --invoked_command="root" + expect-throw "Ambiguous option of 'root sub': --foo conflicts with global option.": + root.check --invoked-command="root" root = cli.Command "root" --options=[ - cli.Option "foo" --short_name="a", + cli.Option "foo" --short-name="a", ] sub = cli.Command "sub" --options=[ - cli.Option "bar" --short_name="a", + cli.Option "bar" --short-name="a", ] --run=(:: null) root.add sub - expect_throw "Ambiguous option of 'root sub': -a conflicts with global option.": - root.check --invoked_command="root" + expect-throw "Ambiguous option of 'root sub': -a conflicts with global option.": + root.check --invoked-command="root" root = cli.Command "root" --options=[ - cli.Option "foo" --short_name="a", + cli.Option "foo" --short-name="a", ] sub = cli.Command "sub" --options=[ - cli.Option "bar" --short_name="ab", + cli.Option "bar" --short-name="ab", ] --run=(:: null) root.add sub - expect_throw "Ambiguous option of 'root sub': -ab conflicts with global option.": - root.check --invoked_command="root" + expect-throw "Ambiguous option of 'root sub': -ab conflicts with global option.": + root.check --invoked-command="root" root = cli.Command "root" --options=[ - cli.Option "machine_32" --short_name="m32", + cli.Option "machine_32" --short-name="m32", ] sub = cli.Command "sub" --options=[ - cli.Option "machine_64" --short_name="m64", + cli.Option "machine_64" --short-name="m64", ] --run=(:: null) root.add sub - root.check --invoked_command="root" + root.check --invoked-command="root" root = cli.Command "root" sub1 := cli.Command "sub1" --options=[ - cli.Option "foo" --short_name="a", + cli.Option "foo" --short-name="a", ] --run=(:: null) root.add sub1 sub2 := cli.Command "sub2" --options=[ - cli.Option "foo" --short_name="a", + cli.Option "foo" --short-name="a", ] --run=(:: null) root.add sub2 root.run ["sub1"] // No error. -ambiguous_command: +ambiguous-command: root := cli.Command "root" sub1 := cli.Command "sub" --run=(:: null) @@ -138,20 +138,20 @@ ambiguous_command: --run=(:: null) root.add sub2 - expect_throw "Ambiguous subcommand of 'root': 'sub'.": - root.check --invoked_command="root" + expect-throw "Ambiguous subcommand of 'root': 'sub'.": + root.check --invoked-command="root" -rest_and_command: +rest-and-command: root := cli.Command "root" --rest=[ cli.Option "rest" --multi, ] sub := cli.Command "sub" --run=(:: null) - expect_throw "Cannot add subcommands to a command with rest arguments.": + expect-throw "Cannot add subcommands to a command with rest arguments.": root.add sub - expect_throw "Cannot have both subcommands and rest arguments.": + expect-throw "Cannot have both subcommands and rest arguments.": root = cli.Command "root" --rest=[ cli.Option "rest" --multi, @@ -161,15 +161,15 @@ rest_and_command: ] --run=(:: null) -run_and_command: +run-and-command: root := cli.Command "root" --run=(:: null) sub := cli.Command "sub" --run=(:: null) - expect_throw "Cannot add subcommands to a command with a run callback.": + expect-throw "Cannot add subcommands to a command with a run callback.": root.add sub - expect_throw "Cannot have both a run callback and subcommands.": + expect-throw "Cannot have both a run callback and subcommands.": root = cli.Command "root" --subcommands=[ cli.Command "sub" --run=(:: null), @@ -183,8 +183,8 @@ rest: cli.Option "other", ] --run=(:: null) - expect_throw "Multi-option 'rest' of 'root' must be the last rest argument.": - root.check --invoked_command="root" + expect-throw "Multi-option 'rest' of 'root' must be the last rest argument.": + root.check --invoked-command="root" root = cli.Command "root" --rest=[ @@ -192,8 +192,8 @@ rest: cli.Option "bar" --required, ] --run=(:: null) - expect_throw "Required rest argument 'bar' of 'root' cannot follow optional rest argument.": - root.check --invoked_command="root" + expect-throw "Required rest argument 'bar' of 'root' cannot follow optional rest argument.": + root.check --invoked-command="root" root = cli.Command "root" --options=[ @@ -203,7 +203,7 @@ rest: cli.Option "foo", ] --run=(:: null) - expect_throw "Rest name 'foo' of 'root' already used.": root.check --invoked_command="root" + expect-throw "Rest name 'foo' of 'root' already used.": root.check --invoked-command="root" root = cli.Command "root" --rest=[ @@ -211,7 +211,7 @@ rest: cli.Option "foo", ] --run=(:: null) - expect_throw "Rest name 'foo' of 'root' already used.": root.check --invoked_command="root" + expect-throw "Rest name 'foo' of 'root' already used.": root.check --invoked-command="root" root = cli.Command "root" --options=[ @@ -223,19 +223,19 @@ rest: ] --run=(:: null) root.add sub - expect_throw "Rest name 'foo' of 'root sub' already a global option.": - root.check --invoked_command="root" + expect-throw "Rest name 'foo' of 'root sub' already a global option.": + root.check --invoked-command="root" -hidden_rest: +hidden-rest: root := cli.Command "root" --rest=[ cli.Option "foo" --hidden, ] --run=(:: null) - expect_throw "Rest argument 'foo' of 'root' cannot be hidden.": - root.check --invoked_command="root" + expect-throw "Rest argument 'foo' of 'root' cannot be hidden.": + root.check --invoked-command="root" -snake_kebab: +snake-kebab: // Test that kebab and snake case lead to ambiguous options. root := cli.Command "root" --options=[ @@ -244,5 +244,5 @@ snake_kebab: ] --run=:: | parsed/cli.Parsed | unreachable - expect_throw "Ambiguous option of 'root': --foo-bar.": - root.check --invoked_command="root" + expect-throw "Ambiguous option of 'root': --foo-bar.": + root.check --invoked-command="root" diff --git a/tests/help_test.toit b/tests/help_test.toit index b4e59c4..18f2422 100644 --- a/tests/help_test.toit +++ b/tests/help_test.toit @@ -3,56 +3,56 @@ // be found in the tests/LICENSE file. import cli -import cli.help_generator_ show HelpGenerator +import cli.help-generator_ show HelpGenerator import expect show * -import .test_ui +import .test-ui main: - test_combination - test_usage - test_aliases - test_commands - test_options - test_examples - -check_output expected/string [block]: + test-combination + test-usage + test-aliases + test-commands + test-options + test-examples + +check-output expected/string [block]: ui := TestUi block.call ui - all_output := ui.messages.join "\n" - if expected != all_output and expected.size == all_output.size: + all-output := ui.messages.join "\n" + if expected != all-output and expected.size == all-output.size: for i := 0; i < expected.size; i++: - if expected[i] != all_output[i]: - print "Mismatch at index $i: '$(string.from_rune expected[i])' != '$(string.from_rune all_output[i])'" + if expected[i] != all-output[i]: + print "Mismatch at index $i: '$(string.from-rune expected[i])' != '$(string.from-rune all-output[i])'" break - expect_equals expected all_output + expect-equals expected all-output -test_combination: - create_root := : | subcommands/List | +test-combination: + create-root := : | subcommands/List | cli.Command "root" --aliases=["r"] // Should not be visible. - --long_help=""" + --long-help=""" Root command. Two lines. """ - --short_help="Root command." // Should not be visible. - --examples= subcommands.is_empty ? [ + --short-help="Root command." // Should not be visible. + --examples= subcommands.is-empty ? [ cli.Example "Example 1:" --arguments="--option1 foo rest" ]: [ cli.Example "Full example:" --arguments="sub --option1 root" ] --options=[ - cli.Option "option1" --short_help="Option 1.", + cli.Option "option1" --short-help="Option 1.", ] - --rest= subcommands.is_empty ? [ - cli.Option "rest1" --short_help="Rest 1" --type="rest_type" --required, + --rest= subcommands.is-empty ? [ + cli.Option "rest1" --short-help="Rest 1" --type="rest_type" --required, ] : [] --subcommands=subcommands - --run= subcommands.is_empty? (:: null) : null + --run= subcommands.is-empty? (:: null) : null - cmd/cli.Command := create_root.call [] + cmd/cli.Command := create-root.call [] - cmd_help := """ + cmd-help := """ Root command. Two lines. @@ -70,34 +70,34 @@ test_combination: # Example 1: root --option1 foo rest """ - check_output cmd_help: | ui/cli.Ui | - cmd.run ["--help"] --ui=ui --invoked_command="root" - expect_equals cmd_help (cmd.help --invoked_command="root") + check-output cmd-help: | ui/cli.Ui | + cmd.run ["--help"] --ui=ui --invoked-command="root" + expect-equals cmd-help (cmd.help --invoked-command="root") sub := cli.Command "sub" --aliases=["sss"] - --long_help="Long sub." - --short_help="Short sub." + --long-help="Long sub." + --short-help="Short sub." --examples=[ cli.Example "Sub Example 1:" --arguments="", cli.Example "Sub Example 2:" --arguments="--option1 foo --option_sub1='xyz'" - --global_priority=5, + --global-priority=5, ] --options=[ - cli.Option "option_sub1" --short_help="Option 1.", - cli.OptionInt "option_sub2" --short_help="Option 2." --default=42, + cli.Option "option_sub1" --short-help="Option 1.", + cli.OptionInt "option_sub2" --short-help="Option 2." --default=42, ] --run=:: null - cmd = create_root.call [sub] + cmd = create-root.call [sub] // Changes to the previous test: // - there is now a `help` subcommand // - the example with global_priority from the sub is here. // The first example also changed, but that's because `create_root` // needs to switch the example. - cmd_help = """ + cmd-help = """ Root command. Two lines. @@ -119,12 +119,12 @@ test_combination: # Sub Example 2: root --option1 foo sub --option_sub1='xyz' """ - check_output cmd_help: | ui/cli.Ui | - cmd.run ["--help"] --ui=ui --invoked_command="root" + check-output cmd-help: | ui/cli.Ui | + cmd.run ["--help"] --ui=ui --invoked-command="root" - expect_equals cmd_help (cmd.help --invoked_command="root") + expect-equals cmd-help (cmd.help --invoked-command="root") - sub_help := """ + sub-help := """ Long sub. Usage: @@ -149,148 +149,148 @@ test_combination: root --option1 foo sub --option_sub1='xyz' """ - check_output sub_help: | ui/cli.Ui | - cmd.run ["help", "sub"] --ui=ui --invoked_command="root" + check-output sub-help: | ui/cli.Ui | + cmd.run ["help", "sub"] --ui=ui --invoked-command="root" -test_usage: - build_usage := : | path/List | - help := HelpGenerator path --invoked_command="root" - help.build_usage - help.to_string +test-usage: + build-usage := : | path/List | + help := HelpGenerator path --invoked-command="root" + help.build-usage + help.to-string cmd := cli.Command "root" --options=[ - cli.Option "option1" --short_help="Option 1." --required, - cli.OptionEnum "option2" ["bar", "baz"] --short_help="Option 2." --required, + cli.Option "option1" --short-help="Option 1." --required, + cli.OptionEnum "option2" ["bar", "baz"] --short-help="Option 2." --required, cli.Flag "optional" ] --rest=[ - cli.Option "rest1" --short_help="Rest 1." --required, - cli.Option "rest2" --short_help="Rest 2.", - cli.Option "rest3" --short_help="Rest 3." --multi, + cli.Option "rest1" --short-help="Rest 1." --required, + cli.Option "rest2" --short-help="Rest 2.", + cli.Option "rest3" --short-help="Rest 3." --multi, ] --run=:: unreachable - expected_usage := """ + expected-usage := """ Usage: root --option1= --option2= [] [--] [] [...] """ - actual_usage := build_usage.call [cmd] - expect_equals expected_usage actual_usage - expect_equals expected_usage "Usage:\n $(cmd.usage --invoked_command="root")\n" + actual-usage := build-usage.call [cmd] + expect-equals expected-usage actual-usage + expect-equals expected-usage "Usage:\n $(cmd.usage --invoked-command="root")\n" // Test different types. cmd = cli.Command "root" --options=[ - cli.Option "option7" --short_help="Option 7." --hidden, - cli.Option "option6" --short_help="Option 6.", - cli.Option "option5" --short_help="Option 5." --required --type="my_type", - cli.Flag "option4" --short_help="Option 4." --required, - cli.OptionEnum "option3" ["bar", "baz"] --short_help="Option 3." --required, - cli.OptionInt "option2" --short_help="Option 2." --required, - cli.Option "option1" --short_help="Option 1." --required, + cli.Option "option7" --short-help="Option 7." --hidden, + cli.Option "option6" --short-help="Option 6.", + cli.Option "option5" --short-help="Option 5." --required --type="my_type", + cli.Flag "option4" --short-help="Option 4." --required, + cli.OptionEnum "option3" ["bar", "baz"] --short-help="Option 3." --required, + cli.OptionInt "option2" --short-help="Option 2." --required, + cli.Option "option1" --short-help="Option 1." --required, ] --run=:: unreachable // Note that options that are not required are not shown in the usage line. // All options are ordered by name. // Since there are named options that aren't shown, there is a []. - expected_usage = """ + expected-usage = """ Usage: root --option1= --option2= --option3= --option4 --option5= [] """ - actual_usage = build_usage.call [cmd] - expect_equals expected_usage actual_usage - expect_equals expected_usage "Usage:\n $(cmd.usage --invoked_command="root")\n" + actual-usage = build-usage.call [cmd] + expect-equals expected-usage actual-usage + expect-equals expected-usage "Usage:\n $(cmd.usage --invoked-command="root")\n" cmd = cli.Command "root" --options=[ - cli.Option "option1" --short_help="Option 1." --required, - cli.Option "option2" --short_help="Option 2." --required, + cli.Option "option1" --short-help="Option 1." --required, + cli.Option "option2" --short-help="Option 2." --required, ] --run=:: unreachable // If all options are required, there is no []. - expected_usage = """ + expected-usage = """ Usage: root --option1= --option2= """ - actual_usage = build_usage.call [cmd] - expect_equals expected_usage actual_usage - expect_equals expected_usage "Usage:\n $(cmd.usage --invoked_command="root")\n" + actual-usage = build-usage.call [cmd] + expect-equals expected-usage actual-usage + expect-equals expected-usage "Usage:\n $(cmd.usage --invoked-command="root")\n" // Test the same options as rest arguments. cmd = cli.Command "root" --rest=[ - cli.Option "option9" --short_help="Option 9." --required, - cli.OptionInt "option2" --short_help="Option 2." --required, - cli.OptionEnum "option3" ["bar", "baz"] --short_help="Option 3." --required, - cli.Flag "option4" --short_help="Option 4." --required, - cli.Option "option5" --short_help="Option 5." --required --type="my_type", - cli.Option "option6" --short_help="Option 6." --required, - cli.Option "option7" --short_help="Option 7.", + cli.Option "option9" --short-help="Option 9." --required, + cli.OptionInt "option2" --short-help="Option 2." --required, + cli.OptionEnum "option3" ["bar", "baz"] --short-help="Option 3." --required, + cli.Flag "option4" --short-help="Option 4." --required, + cli.Option "option5" --short-help="Option 5." --required --type="my_type", + cli.Option "option6" --short-help="Option 6." --required, + cli.Option "option7" --short-help="Option 7.", ] --run=:: unreachable // Rest arguments must not be sorted. // Also, optional arguments are shown. - expected_usage = """ + expected-usage = """ Usage: root [--] [] """ - actual_usage = build_usage.call [cmd] - expect_equals expected_usage actual_usage - expect_equals expected_usage "Usage:\n $(cmd.usage --invoked_command="root")\n" + actual-usage = build-usage.call [cmd] + expect-equals expected-usage actual-usage + expect-equals expected-usage "Usage:\n $(cmd.usage --invoked-command="root")\n" cmd = cli.Command "root" --options=[ - cli.Flag "option3" --short_help="Option 3." --required, - cli.Option "option2" --short_help="Option 2.", - cli.Option "option1" --short_help="Option 1." --required, + cli.Flag "option3" --short-help="Option 3." --required, + cli.Option "option2" --short-help="Option 2.", + cli.Option "option1" --short-help="Option 1." --required, ] sub := cli.Command "sub" --options=[ - cli.Flag "sub_option3" --short_help="Option 3." --required, - cli.Option "sub_option2" --short_help="Option 2.", - cli.Option "sub_option1" --short_help="Option 1." --required, + cli.Flag "sub_option3" --short-help="Option 3." --required, + cli.Option "sub_option2" --short-help="Option 2.", + cli.Option "sub_option1" --short-help="Option 1." --required, ] --run=:: unreachable cmd.add sub // Test that global options are correctly shown when they are required. // All options must still be sorted. - expected_usage = """ + expected-usage = """ Usage: root --option1= --option3 sub --sub-option1= --sub-option3 [] """ - actual_usage = build_usage.call [cmd, sub] - expect_equals expected_usage actual_usage + actual-usage = build-usage.call [cmd, sub] + expect-equals expected-usage actual-usage - expected_cmd_usage := "sub --sub-option1= --sub-option3 []" - expect_equals expected_cmd_usage (sub.usage --invoked_command="sub") + expected-cmd-usage := "sub --sub-option1= --sub-option3 []" + expect-equals expected-cmd-usage (sub.usage --invoked-command="sub") cmd = cli.Command "root" --usage="overridden use line" --run=:: unreachable - expected_usage = """ + expected-usage = """ Usage: overridden use line """ - actual_usage = build_usage.call [cmd] - expect_equals expected_usage actual_usage - expect_equals expected_usage "Usage:\n $(cmd.usage --invoked_command="root")\n" + actual-usage = build-usage.call [cmd] + expect-equals expected-usage actual-usage + expect-equals expected-usage "Usage:\n $(cmd.usage --invoked-command="root")\n" -test_aliases: - build_aliases := : | path/List | - help := HelpGenerator path --invoked_command="root" - help.build_aliases - help.to_string +test-aliases: + build-aliases := : | path/List | + help := HelpGenerator path --invoked-command="root" + help.build-aliases + help.to-string cmd := cli.Command "root" --aliases=["alias1", "alias2"] --run=:: unreachable // The aliases for the root command are not shown. - expect_equals "" (build_aliases.call [cmd]) + expect-equals "" (build-aliases.call [cmd]) cmd = cli.Command "root" sub := cli.Command "sub" @@ -302,7 +302,7 @@ test_aliases: Aliases: alias1 """ - expect_equals expected (build_aliases.call [cmd, sub]) + expect-equals expected (build-aliases.call [cmd, sub]) cmd = cli.Command "root" sub = cli.Command "sub" @@ -314,18 +314,18 @@ test_aliases: Aliases: alias1, alias2 """ - expect_equals expected (build_aliases.call [cmd, sub]) + expect-equals expected (build-aliases.call [cmd, sub]) -test_commands: - build_commands := : | path/List | - help := HelpGenerator path --invoked_command="root" - help.build_commands - help.to_string +test-commands: + build-commands := : | path/List | + help := HelpGenerator path --invoked-command="root" + help.build-commands + help.to-string cmd := cli.Command "root" --run=:: unreachable // If the root command has a run function there is no subcommand section. - expect_equals "" (build_commands.call [cmd]) + expect-equals "" (build-commands.call [cmd]) cmd = cli.Command "root" sub := cli.Command "sub" @@ -337,11 +337,11 @@ test_commands: help Show help for a command. sub """ - expect_equals expected (build_commands.call [cmd]) + expect-equals expected (build-commands.call [cmd]) cmd = cli.Command "root" sub = cli.Command "sub" - --short_help="Subcommand." + --short-help="Subcommand." --run=:: unreachable cmd.add sub @@ -350,14 +350,14 @@ test_commands: help Show help for a command. sub Subcommand. """ - expect_equals expected (build_commands.call [cmd]) + expect-equals expected (build-commands.call [cmd]) sub2 := cli.Command "sub2" - --short_help="Subcommand 2." + --short-help="Subcommand 2." --run=:: unreachable cmd.add sub2 sub3 := cli.Command "asub3" - --short_help="Subcommand 3." + --short-help="Subcommand 3." --run=:: unreachable cmd.add sub3 @@ -369,11 +369,11 @@ test_commands: sub Subcommand. sub2 Subcommand 2. """ - expect_equals expected (build_commands.call [cmd]) + expect-equals expected (build-commands.call [cmd]) cmd = cli.Command "root" sub = cli.Command "sub" - --long_help=""" + --long-help=""" First paragraph. @@ -390,23 +390,23 @@ test_commands: sub First paragraph. """ - expect_equals expected (build_commands.call [cmd]) + expect-equals expected (build-commands.call [cmd]) cmd = cli.Command "root" sub = cli.Command "sub" --run=:: unreachable cmd.add sub sub2 = cli.Command "sub2" - --long_help="unused." - --short_help=""" + --long-help="unused." + --short-help=""" Long shorthelp. """ --run=:: unreachable cmd.add sub2 sub3 = cli.Command "sub3" - --long_help="unused." - --short_help="Short help3." + --long-help="unused." + --short-help="Short help3." --run=:: unreachable cmd.add sub3 @@ -418,11 +418,11 @@ test_commands: shorthelp. sub3 Short help3. """ - expect_equals expected (build_commands.call [cmd]) + expect-equals expected (build-commands.call [cmd]) cmd = cli.Command "root" sub = cli.Command "help" - --short_help="My own help." + --short-help="My own help." --run=:: unreachable cmd.add sub @@ -431,12 +431,12 @@ test_commands: Commands: help My own help. """ - expect_equals expected (build_commands.call [cmd]) + expect-equals expected (build-commands.call [cmd]) cmd = cli.Command "root" sub = cli.Command "sub" --aliases=["help"] - --short_help="Sub with 'help' alias." + --short-help="Sub with 'help' alias." --run=:: unreachable cmd.add sub @@ -445,58 +445,58 @@ test_commands: Commands: sub Sub with 'help' alias. """ - expect_equals expected (build_commands.call [cmd]) + expect-equals expected (build-commands.call [cmd]) -test_options: - build_local_options := : | path/List | - help := HelpGenerator path --invoked_command="root" - help.build_local_options - help.to_string +test-options: + build-local-options := : | path/List | + help := HelpGenerator path --invoked-command="root" + help.build-local-options + help.to-string - build_global_options := : | path/List | - help := HelpGenerator path --invoked_command="root" - help.build_global_options - help.to_string + build-global-options := : | path/List | + help := HelpGenerator path --invoked-command="root" + help.build-global-options + help.to-string // Try different options of types: int, string, enum, booleans (flags). // Try different flags, like --required, --short_help, --type, --default, multi. cmd := cli.Command "root" --options=[ - cli.OptionInt "option1" --short_help="Option 1." --default=42, - cli.Option "option2" --short_help="Option 2." --default="foo", - cli.OptionEnum "option3" ["bar", "baz"] --short_name="x" --short_help="Option 3." --default="bar", - cli.Flag "option4" --short_name="4" --short_help="Option 4." --default=false, - cli.Flag "option5" --short_help="Option 5." --default=true, - - cli.OptionInt "option6" --short_help="Option 6." --required, - cli.Option "option7" --short_help="Option 7." --required, - cli.OptionEnum "option8" ["bar", "baz"] --short_help="Option 8." --required, - cli.Flag "option9" --short_help="Option 9." --required, - - cli.OptionInt "option10" --short_help="Option 10." --multi, - cli.Option "option11" --short_help="Option 11." --multi, - cli.OptionEnum "option12" ["bar", "baz"] --short_help="Option 12." --multi, - cli.Flag "option13" --short_help="Option 13." --multi, - - cli.OptionInt "option14" --short_help="Option 14." --multi --required, - cli.Option "option15" --short_help="Option 15." --multi --required, - cli.OptionEnum "option16" ["bar", "baz"] --short_help="Option 16." --multi --required, - cli.Flag "option17" --short_help="Option 17." --multi --required, - - cli.OptionInt "option18" --short_help="Option 18." --type="my_int_type", - cli.Option "option19" --short_help="Option 19." --short_name="y" --type="my_string_type", - cli.OptionEnum "option20" ["bar", "baz"] --short_help="Option 20." --type="my_enum_type", - - cli.OptionInt "option21" --short_help="Option 21\nmulti_line_help.", - - cli.OptionInt "option22" --short_name="zz" --short_help="Option 22." --default=42, + cli.OptionInt "option1" --short-help="Option 1." --default=42, + cli.Option "option2" --short-help="Option 2." --default="foo", + cli.OptionEnum "option3" ["bar", "baz"] --short-name="x" --short-help="Option 3." --default="bar", + cli.Flag "option4" --short-name="4" --short-help="Option 4." --default=false, + cli.Flag "option5" --short-help="Option 5." --default=true, + + cli.OptionInt "option6" --short-help="Option 6." --required, + cli.Option "option7" --short-help="Option 7." --required, + cli.OptionEnum "option8" ["bar", "baz"] --short-help="Option 8." --required, + cli.Flag "option9" --short-help="Option 9." --required, + + cli.OptionInt "option10" --short-help="Option 10." --multi, + cli.Option "option11" --short-help="Option 11." --multi, + cli.OptionEnum "option12" ["bar", "baz"] --short-help="Option 12." --multi, + cli.Flag "option13" --short-help="Option 13." --multi, + + cli.OptionInt "option14" --short-help="Option 14." --multi --required, + cli.Option "option15" --short-help="Option 15." --multi --required, + cli.OptionEnum "option16" ["bar", "baz"] --short-help="Option 16." --multi --required, + cli.Flag "option17" --short-help="Option 17." --multi --required, + + cli.OptionInt "option18" --short-help="Option 18." --type="my_int_type", + cli.Option "option19" --short-help="Option 19." --short-name="y" --type="my_string_type", + cli.OptionEnum "option20" ["bar", "baz"] --short-help="Option 20." --type="my_enum_type", + + cli.OptionInt "option21" --short-help="Option 21\nmulti_line_help.", + + cli.OptionInt "option22" --short-name="zz" --short-help="Option 22." --default=42, ] sub := cli.Command "sub" --run=:: unreachable cmd.add sub // Note that all required arguments are in the usage line. - expected_options := """ + expected-options := """ Options: -h, --help Show help for this command. --option1 int Option 1. (default: 42) @@ -523,16 +523,16 @@ test_options: --option8 bar|baz Option 8. (required) --option9 Option 9. (required) """ - actual_options := build_local_options.call [cmd] - expect_equals expected_options actual_options + actual-options := build-local-options.call [cmd] + expect-equals expected-options actual-options - expected_options = "" - actual_options = build_global_options.call [cmd] - expect_equals expected_options actual_options + expected-options = "" + actual-options = build-global-options.call [cmd] + expect-equals expected-options actual-options // Pretty much the same as the local options. // Title changes and the `--help` flag is gone. - expected_options = """ + expected-options = """ Global options: --option1 int Option 1. (default: 42) --option10 int Option 10. (multi) @@ -558,39 +558,39 @@ test_options: --option8 bar|baz Option 8. (required) --option9 Option 9. (required) """ - actual_options = build_global_options.call [cmd, sub] - expect_equals expected_options actual_options + actual-options = build-global-options.call [cmd, sub] + expect-equals expected-options actual-options // Test global options. cmd = cli.Command "root" --options=[ - cli.OptionInt "option1" --short_help="Option 1." --default=42, + cli.OptionInt "option1" --short-help="Option 1." --default=42, ] sub = cli.Command "sub" --options=[ - cli.OptionInt "option_sub1" --short_help="Option 1." --default=42, + cli.OptionInt "option_sub1" --short-help="Option 1." --default=42, ] --run=:: unreachable cmd.add sub - sub_local_expected := """ + sub-local-expected := """ Options: -h, --help Show help for this command. --option-sub1 int Option 1. (default: 42) """ - sub_global_expected := """ + sub-global-expected := """ Global options: --option1 int Option 1. (default: 42) """ - sub_local_actual := build_local_options.call [cmd, sub] - sub_global_actual := build_global_options.call [cmd, sub] - expect_equals sub_local_expected sub_local_actual - expect_equals sub_global_expected sub_global_actual + sub-local-actual := build-local-options.call [cmd, sub] + sub-global-actual := build-global-options.call [cmd, sub] + expect-equals sub-local-expected sub-local-actual + expect-equals sub-global-expected sub-global-actual cmd = cli.Command "root" --options=[ - cli.OptionInt "option1" --short_name="h" --short_help="Option 1." --default=42, + cli.OptionInt "option1" --short-name="h" --short-help="Option 1." --default=42, ] --run=:: unreachable expected := """ @@ -598,13 +598,13 @@ test_options: --help Show help for this command. -h, --option1 int Option 1. (default: 42) """ - actual := build_local_options.call [cmd] - expect_equals expected actual + actual := build-local-options.call [cmd] + expect-equals expected actual cmd = cli.Command "root" --options=[ - cli.OptionInt "option1" --short_help="Option 1." --default=42, - cli.OptionEnum "help" ["bar", "baz"] --short_help="Own help." + cli.OptionInt "option1" --short-help="Option 1." --default=42, + cli.OptionEnum "help" ["bar", "baz"] --short-help="Own help." ] --run=:: unreachable // No automatic help. Not even `-h`. @@ -613,18 +613,18 @@ test_options: --help bar|baz Own help. --option1 int Option 1. (default: 42) """ - actual = build_local_options.call [cmd] - expect_equals expected actual + actual = build-local-options.call [cmd] + expect-equals expected actual -test_examples: - build_examples := : | path/List | - help := HelpGenerator path --invoked_command="root" - help.build_examples - help.to_string +test-examples: + build-examples := : | path/List | + help := HelpGenerator path --invoked-command="root" + help.build-examples + help.to-string cmd := cli.Command "root" --options=[ - cli.OptionInt "option1" --short_help="Option 1." --default=42, + cli.OptionInt "option1" --short-help="Option 1." --default=42, ] --examples=[ cli.Example "Example 1:" --arguments="--option1=499", @@ -649,24 +649,24 @@ test_examples: root --option1=1 root --option1=2 """ - actual := build_examples.call [cmd] - expect_equals expected actual + actual := build-examples.call [cmd] + expect-equals expected actual // Example arguments are moved to the command that defines them. cmd = cli.Command "root" --options=[ - cli.OptionInt "option1" --short_name="x", - cli.Flag "aa" --short_name="a", + cli.OptionInt "option1" --short-name="x", + cli.Flag "aa" --short-name="a", ] sub := cli.Command "sub" --options=[ - cli.OptionInt "option_sub1" --short_name="y", - cli.Flag "bb" --short_name="b", + cli.OptionInt "option_sub1" --short-name="y", + cli.Flag "bb" --short-name="b", ] sub2 := cli.Command "subsub" --options=[ - cli.OptionInt "option_subsub1" --short_name="z", - cli.Flag "cc" --short_name="c", + cli.OptionInt "option_subsub1" --short-name="z", + cli.Flag "cc" --short-name="c", ] --rest=[ cli.Option "rest" --multi @@ -731,8 +731,8 @@ test_examples: root sub -b subsub -cx 42 root -a sub subsub -bz 55 """ - actual = build_examples.call [cmd, sub, sub2] - expect_equals expected actual + actual = build-examples.call [cmd, sub, sub2] + expect-equals expected actual // Verify that examples of subcommands are used if they have a global_priority. // Also check that they are sorted by priority. @@ -744,9 +744,9 @@ test_examples: ] sub = cli.Command "sub" --examples=[ - cli.Example "Example 1:" --arguments="global3" --global_priority=3, + cli.Example "Example 1:" --arguments="global3" --global-priority=3, cli.Example "Example 2:" --arguments="no_global", - cli.Example "Example 3:" --arguments="global1" --global_priority=1, + cli.Example "Example 3:" --arguments="global1" --global-priority=1, ] --rest=[ cli.Option "rest" @@ -756,8 +756,8 @@ test_examples: sub2 = cli.Command "sub2" --examples=[ cli.Example "Example 4:" --arguments="no_global", - cli.Example "Example 5:" --arguments="global5" --global_priority=5, - cli.Example "Example 6:" --arguments="global1" --global_priority=1, + cli.Example "Example 5:" --arguments="global5" --global-priority=5, + cli.Example "Example 6:" --arguments="global1" --global-priority=1, ] --rest=[ cli.Option "rest" @@ -785,5 +785,5 @@ test_examples: # Example 5: root sub2 global5 """ - actual = build_examples.call [cmd] - expect_equals expected actual + actual = build-examples.call [cmd] + expect-equals expected actual diff --git a/tests/options_test.toit b/tests/options_test.toit index 7195394..b5a3a4b 100644 --- a/tests/options_test.toit +++ b/tests/options_test.toit @@ -6,139 +6,139 @@ import cli import expect show * main: - test_string - test_enum - test_int - test_flag - test_bad_combos + test-string + test-enum + test-int + test-flag + test-bad-combos -test_string: +test-string: option := cli.Option "foo" - expect_equals option.name "foo" - expect_null option.default - expect_equals "string" option.type - expect_null option.short_name - expect_null option.short_help - expect_not option.is_required - expect_not option.is_hidden - expect_not option.is_multi - expect_not option.should_split_commas - expect_not option.is_flag + expect-equals option.name "foo" + expect-null option.default + expect-equals "string" option.type + expect-null option.short-name + expect-null option.short-help + expect-not option.is-required + expect-not option.is-hidden + expect-not option.is-multi + expect-not option.should-split-commas + expect-not option.is-flag option = cli.Option "foo" --default="some_default" - expect_equals "some_default" option.default + expect-equals "some_default" option.default - option = cli.Option "foo" --short_name="f" - expect_equals "f" option.short_name + option = cli.Option "foo" --short-name="f" + expect-equals "f" option.short-name - option = cli.Option "foo" --short_name="foo" - expect_equals "foo" option.short_name + option = cli.Option "foo" --short-name="foo" + expect-equals "foo" option.short-name - option = cli.Option "foo" --short_help="Some_help." - expect_equals "Some_help." option.short_help + option = cli.Option "foo" --short-help="Some_help." + expect-equals "Some_help." option.short-help option = cli.Option "foo" --required - expect option.is_required + expect option.is-required option = cli.Option "foo" --hidden - expect option.is_hidden + expect option.is-hidden option = cli.Option "foo" --multi - expect option.is_multi - - option = cli.Option "foo" --multi --split_commas - expect option.should_split_commas - - option = cli.Option "foo" --short_name="f" \ - --short_help="Baz." --required --multi \ - --split_commas --type="some_type" - expect_equals option.name "foo" - expect_equals "some_type" option.type - expect_equals option.short_name "f" - expect_equals option.short_help "Baz." - expect option.is_required - expect_not option.is_hidden - expect option.is_multi - expect option.should_split_commas - expect_not option.is_flag + expect option.is-multi + + option = cli.Option "foo" --multi --split-commas + expect option.should-split-commas + + option = cli.Option "foo" --short-name="f" \ + --short-help="Baz." --required --multi \ + --split-commas --type="some_type" + expect-equals option.name "foo" + expect-equals "some_type" option.type + expect-equals option.short-name "f" + expect-equals option.short-help "Baz." + expect option.is-required + expect-not option.is-hidden + expect option.is-multi + expect option.should-split-commas + expect-not option.is-flag value := option.parse "foo" - expect_equals "foo" value + expect-equals "foo" value -test_enum: +test-enum: option := cli.OptionEnum "enum" ["foo", "bar"] - expect_equals option.name "enum" - expect_null option.default - expect_equals "foo|bar" option.type + expect-equals option.name "enum" + expect-null option.default + expect-equals "foo|bar" option.type option = cli.OptionEnum "enum" ["foo", "bar"] --default="bar" - expect_equals "bar" option.default + expect-equals "bar" option.default value := option.parse "foo" - expect_equals "foo" value + expect-equals "foo" value value = option.parse "bar" - expect_equals "bar" value + expect-equals "bar" value - expect_throw "Invalid value for option 'enum': 'baz'. Valid values are: foo, bar.": + expect-throw "Invalid value for option 'enum': 'baz'. Valid values are: foo, bar.": option.parse "baz" -test_int: +test-int: option := cli.OptionInt "int" - expect_equals option.name "int" - expect_null option.default - expect_equals "int" option.type + expect-equals option.name "int" + expect-null option.default + expect-equals "int" option.type option = cli.OptionInt "int" --default=42 - expect_equals 42 option.default + expect-equals 42 option.default value := option.parse "42" - expect_equals 42 value + expect-equals 42 value - expect_throw "Invalid integer value for option 'int': 'foo'.": + expect-throw "Invalid integer value for option 'int': 'foo'.": option.parse "foo" -test_flag: +test-flag: flag := cli.Flag "flag" --default=false - expect_equals flag.name "flag" - expect_identical false flag.default + expect-equals flag.name "flag" + expect-identical false flag.default flag = cli.Flag "flag" --default=true - expect_identical true flag.default + expect-identical true flag.default value := flag.parse "true" - expect_identical true value + expect-identical true value value = flag.parse "false" - expect_identical false value + expect-identical false value - expect_throw "Invalid value for boolean flag 'flag': 'foo'. Valid values are: true, false.": + expect-throw "Invalid value for boolean flag 'flag': 'foo'. Valid values are: true, false.": flag.parse "foo" -test_bad_combos: - expect_throw "--split_commas is only valid for multi options.": - cli.Option "foo" --split_commas +test-bad-combos: + expect-throw "--split_commas is only valid for multi options.": + cli.Option "foo" --split-commas - expect_throw "Invalid short option name: '@'": - cli.Option "bar" --short_name="@" + expect-throw "Invalid short option name: '@'": + cli.Option "bar" --short-name="@" - expect_throw "Option can't be hidden and required.": + expect-throw "Option can't be hidden and required.": cli.Option "foo" --hidden --required - expect_throw "Option can't have default value and be required.": + expect-throw "Option can't have default value and be required.": cli.Option "foo" --default="bar" --required - expect_throw "Option can't have default value and be required.": + expect-throw "Option can't have default value and be required.": cli.OptionInt "foo" --default=42 --required - expect_throw "Option can't have default value and be required.": + expect-throw "Option can't have default value and be required.": cli.Flag "foo" --default=false --required - expect_throw "Multi option can't have default value.": + expect-throw "Multi option can't have default value.": cli.Option "foo" --default="bar" --multi - expect_throw "Multi option can't have default value.": + expect-throw "Multi option can't have default value.": cli.OptionInt "foo" --default=42 --multi - expect_throw "Multi option can't have default value.": + expect-throw "Multi option can't have default value.": cli.Flag "foo" --default=false --multi diff --git a/tests/parser_test.toit b/tests/parser_test.toit index e7537e0..b9795d1 100644 --- a/tests/parser_test.toit +++ b/tests/parser_test.toit @@ -5,36 +5,36 @@ import cli import expect show * -import .test_ui +import .test-ui -check_arguments expected/Map parsed/cli.Parsed: +check-arguments expected/Map parsed/cli.Parsed: expected.do: | key value | - expect_equals value parsed[key] + expect-equals value parsed[key] main: - test_options - test_multi - test_rest - test_no_option - test_invert_flag - test_invert_non_flag - test_value_for_flag - test_missing_args - test_missing_subcommand - test_dash_arg - test_mixed_rest_named - test_snake_kebab - -test_options: + test-options + test-multi + test-rest + test-no-option + test-invert-flag + test-invert-non-flag + test-value-for-flag + test-missing-args + test-missing-subcommand + test-dash-arg + test-mixed-rest-named + test-snake-kebab + +test-options: expected /Map? := null cmd := cli.Command "test" --options=[ - cli.Option "foo" --short_name="f" --default="default_foo", - cli.Option "bar" --short_name="b", - cli.OptionInt "gee" --short_name="g", + cli.Option "foo" --short-name="f" --default="default_foo", + cli.Option "bar" --short-name="b", + cli.OptionInt "gee" --short-name="g", ] --run=:: | parsed/cli.Parsed | - check_arguments expected parsed + check-arguments expected parsed expected = {"foo": "foo_value", "bar": "bar_value", "gee": null} cmd.run ["-f", "foo_value", "-b", "bar_value"] @@ -53,12 +53,12 @@ test_options: cmd = cli.Command "test" --options=[ - cli.Option "foo" --short_name="f" --default="default_foo", - cli.Flag "bar" --short_name="b", - cli.Option "fizz" --short_name="iz" --default="default_fizz", + cli.Option "foo" --short-name="f" --default="default_foo", + cli.Flag "bar" --short-name="b", + cli.Option "fizz" --short-name="iz" --default="default_fizz", ] --run=:: | parsed/cli.Parsed | - check_arguments expected parsed + check-arguments expected parsed expected = {"foo": "default_foo", "bar": true, "fizz": "default_fizz"} cmd.run ["-b"] @@ -85,10 +85,10 @@ test_options: cmd = cli.Command "test" --options=[ - cli.Flag "foo" --short_name="f" --default=false, + cli.Flag "foo" --short-name="f" --default=false, ] --run=:: | parsed/cli.Parsed | - check_arguments expected parsed + check-arguments expected parsed expected = {"foo": false} cmd.run [] @@ -101,24 +101,24 @@ test_options: cmd = cli.Command "test" --options=[ - cli.Flag "foo" --short_name="f" --default=true, + cli.Flag "foo" --short-name="f" --default=true, ] --run=:: | parsed/cli.Parsed | - check_arguments expected parsed + check-arguments expected parsed expected = {"foo": true } cmd.run [] -test_multi: +test-multi: expected/Map? := null cmd := cli.Command "test" --options=[ - cli.Option "foo" --short_name="f" --multi, - cli.Option "bar" --short_name="b" --multi --split_commas, + cli.Option "foo" --short-name="f" --multi, + cli.Option "bar" --short-name="b" --multi --split-commas, ] --run=:: | parsed/cli.Parsed | - check_arguments expected parsed + check-arguments expected parsed expected = {"foo": ["foo_value"], "bar": ["bar_value"]} cmd.run ["-f", "foo_value", "-b", "bar_value"] @@ -145,10 +145,10 @@ test_multi: cmd = cli.Command "test" --options=[ - cli.OptionInt "foo" --short_name="f" --multi --split_commas, + cli.OptionInt "foo" --short-name="f" --multi --split-commas, ] --run=:: | parsed/cli.Parsed | - check_arguments expected parsed + check-arguments expected parsed expected = {"foo": [1, 2, 3]} cmd.run ["-f", "1", "-f", "2", "-f", "3"] @@ -156,10 +156,10 @@ test_multi: cmd = cli.Command "test" --options=[ - cli.Flag "foo" --short_name="f" --multi, + cli.Flag "foo" --short-name="f" --multi, ] --run=:: | parsed/cli.Parsed | - check_arguments expected parsed + check-arguments expected parsed expected = {"foo": [true, true, true]} cmd.run ["-f", "-f", "-f"] @@ -168,15 +168,15 @@ test_multi: cmd = cli.Command "test" --options=[ - cli.OptionEnum "foo" ["a", "b"] --short_name="f" --multi, + cli.OptionEnum "foo" ["a", "b"] --short-name="f" --multi, ] --run=:: | parsed/cli.Parsed | - check_arguments expected parsed + check-arguments expected parsed expected = {"foo": ["a", "b", "a"]} cmd.run ["-f", "a", "-f", "b", "-f", "a"] -test_rest: +test-rest: expected/Map? := null cmd := cli.Command "test" --rest=[ @@ -184,7 +184,7 @@ test_rest: cli.OptionInt "bar", ] --run=:: | parsed/cli.Parsed | - check_arguments expected parsed + check-arguments expected parsed expected = {"foo": "foo_value", "bar": 42} cmd.run ["foo_value", "42"] @@ -201,7 +201,7 @@ test_rest: cli.OptionInt "bar" --default=42, ] --run=:: | parsed/cli.Parsed | - check_arguments expected parsed + check-arguments expected parsed expected = {"foo": "foo_value", "bar": 42} cmd.run ["foo_value"] @@ -209,7 +209,7 @@ test_rest: expected = {"foo": "foo_value", "bar": 43} cmd.run ["foo_value", "43"] - expect_abort "Missing required rest argument: 'foo'.": | ui/cli.Ui | + expect-abort "Missing required rest argument: 'foo'.": | ui/cli.Ui | cmd.run [] --ui=ui cmd = cli.Command "test" @@ -218,7 +218,7 @@ test_rest: cli.Option "bar" --required --multi, ] --run=:: | parsed/cli.Parsed | - check_arguments expected parsed + check-arguments expected parsed expected = {"foo": "foo_value", "bar": ["bar_value"]} cmd.run ["foo_value", "bar_value"] @@ -226,33 +226,33 @@ 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 | + expect-abort "Missing required rest argument: 'bar'.": | ui/cli.Ui | cmd.run ["foo_value"] --ui=ui cmd = cli.Command "test" --run=:: null - expect_abort "Unexpected rest argument: 'baz'.": | ui/cli.Ui | + expect-abort "Unexpected rest argument: 'baz'.": | ui/cli.Ui | cmd.run ["baz"] --ui=ui -test_subcommands: +test-subcommands: expected/Map? := null cmd := cli.Command "test" --subcommands=[ cli.Command "sub1" --options=[ - cli.Option "foo" --short_name="f", + cli.Option "foo" --short-name="f", ] --run=:: | parsed/cli.Parsed | - check_arguments expected parsed, + check-arguments expected parsed, cli.Command "sub2" --options=[ - cli.Option "bar" --short_name="b", + cli.Option "bar" --short-name="b", ] --run=:: | parsed/cli.Parsed | - check_arguments expected parsed, + check-arguments expected parsed, ] --run=:: | parsed/cli.Parsed | - check_arguments expected parsed + check-arguments expected parsed expected = {"foo": "foo_value"} cmd.run ["sub1", "-f", "foo_value"] @@ -268,36 +268,36 @@ test_subcommands: expected = {"bar": null} cmd.run ["sub2"] -test_no_option: +test-no-option: cmd := cli.Command "test" --run=:: | parsed/cli.Parsed | - expect_throw "No option named 'foo'": parsed["foo"] + expect-throw "No option named 'foo'": parsed["foo"] cmd.run [] cmd = cli.Command "test" --options=[ - cli.Option "foo" --short_name="f", + cli.Option "foo" --short-name="f", ] --subcommands=[ cli.Command "sub1" --options=[ - cli.Option "bar" --short_name="b", + cli.Option "bar" --short-name="b", ] --run=:: | parsed/cli.Parsed | - expect_throw "No option named 'gee'": parsed["gee"], + expect-throw "No option named 'gee'": parsed["gee"], ] cmd.run ["sub1", "-b", "bar_value"] cmd.run ["sub1"] -test_invert_flag: +test-invert-flag: expected/Map? := null cmd := cli.Command "test" --options=[ - cli.Flag "foo" --short_name="f", + cli.Flag "foo" --short-name="f", ] --run=:: | parsed/cli.Parsed | - check_arguments expected parsed + check-arguments expected parsed expected = {"foo": null} cmd.run [] @@ -310,10 +310,10 @@ test_invert_flag: cmd = cli.Command "test" --options=[ - cli.Flag "foo" --short_name="f" --default=true, + cli.Flag "foo" --short-name="f" --default=true, ] --run=:: | parsed/cli.Parsed | - check_arguments expected parsed + check-arguments expected parsed expected = {"foo": true} cmd.run [] @@ -321,63 +321,63 @@ test_invert_flag: expected = {"foo": false} cmd.run ["--no-foo"] -test_invert_non_flag: +test-invert-non-flag: cmd := cli.Command "test" --options=[ - cli.Option "foo" --short_name="f", + cli.Option "foo" --short-name="f", ] --run=:: | parsed/cli.Parsed | unreachable - expect_abort "Cannot invert non-boolean flag --foo.": | ui/cli.Ui | + expect-abort "Cannot invert non-boolean flag --foo.": | ui/cli.Ui | cmd.run ["--no-foo"] --ui=ui -test_value_for_flag: +test-value-for-flag: cmd := cli.Command "test" --options=[ - cli.Flag "foo" --short_name="f", + cli.Flag "foo" --short-name="f", ] --run=:: | parsed/cli.Parsed | unreachable - expect_abort "Cannot specify value for boolean flag --foo.": | ui/cli.Ui | + expect-abort "Cannot specify value for boolean flag --foo.": | ui/cli.Ui | cmd.run ["--foo=bar"] --ui=ui -test_missing_args: +test-missing-args: cmd := cli.Command "test" --options=[ - cli.Option "foo" --short_name="f", + cli.Option "foo" --short-name="f", ] --run=:: | parsed/cli.Parsed | unreachable - expect_abort "Option --foo requires an argument.": | ui/cli.Ui | + expect-abort "Option --foo requires an argument.": | ui/cli.Ui | cmd.run ["--foo"] --ui=ui - expect_abort "Option -f requires an argument.": | ui/cli.Ui | + expect-abort "Option -f requires an argument.": | ui/cli.Ui | cmd.run ["-f"] --ui=ui -test_missing_subcommand: +test-missing-subcommand: cmd := cli.Command "test" --subcommands=[ cli.Command "sub1" --run=:: unreachable ] - expect_abort "Missing subcommand.": | ui/cli.Ui | + expect-abort "Missing subcommand.": | ui/cli.Ui | cmd.run [] --ui=ui -test_dash_arg: +test-dash-arg: cmd := cli.Command "test" --options=[ - cli.Option "foo" --short_name="f", + cli.Option "foo" --short-name="f", ] --run=:: | parsed/cli.Parsed | - check_arguments {"foo": "-"} parsed + check-arguments {"foo": "-"} parsed cmd.run ["-f", "-"] -test_mixed_rest_named: +test-mixed-rest-named: // Rest arguments can be mixed with named arguments as long as there isn't a '--'. cmd := cli.Command "test" @@ -389,7 +389,7 @@ test_mixed_rest_named: cli.Option "baz" --required, ] --run=:: | parsed/cli.Parsed | - check_arguments {"foo": "foo_value", "bar": "bar_value", "baz": "baz_value"} parsed + check-arguments {"foo": "foo_value", "bar": "bar_value", "baz": "baz_value"} parsed cmd.run ["--foo", "foo_value", "--bar", "bar_value", "baz_value"] cmd.run ["baz_value", "--foo", "foo_value", "--bar", "bar_value"] @@ -404,20 +404,20 @@ test_mixed_rest_named: cli.Option "baz" --required, ] --run=:: | parsed/cli.Parsed | - check_arguments {"foo": "foo_value", "bar": "bar_value", "baz": "--foo"} parsed + check-arguments {"foo": "foo_value", "bar": "bar_value", "baz": "--foo"} parsed // Because of the '--', the rest argument is not interpreted as a named argument. cmd.run ["--foo", "foo_value", "--bar", "bar_value", "--", "--foo"] -test_snake_kebab: +test-snake-kebab: cmd := cli.Command "test" --options=[ - cli.Option "foo-bar" --short_name="f", + cli.Option "foo-bar" --short-name="f", cli.Option "toto_titi" ] --run=:: | 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 + check-arguments {"foo-bar": "foo_value", "toto-titi": "toto_value" } parsed + check-arguments {"foo_bar": "foo_value", "toto_titi": "toto_value" } parsed 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 d9d81a1..de45800 100644 --- a/tests/subcommand_test.toit.toit +++ b/tests/subcommand_test.toit.toit @@ -5,27 +5,27 @@ import cli import expect show * -check_arguments expected/Map parsed/cli.Parsed: +check-arguments expected/Map parsed/cli.Parsed: expected.do: | key value | - expect_equals value parsed[key] + expect-equals value parsed[key] main: cmd := cli.Command "root" --options=[ - cli.Option "global_string" --short_name="g" --short_help="Global string." --required, - cli.Option "global_string2" --short_help="Global string2.", + cli.Option "global_string" --short-name="g" --short-help="Global string." --required, + cli.Option "global_string2" --short-help="Global string2.", ] expected := {:} - executed_sub := false + executed-sub := false sub := cli.Command "sub1" --options=[ - cli.Option "sub_string" --short_name="s" --short_help="Sub string." --required, + cli.Option "sub_string" --short-name="s" --short-help="Sub string." --required, ] --run=:: | arguments | - executed_sub = true - check_arguments expected arguments + executed-sub = true + check-arguments expected arguments cmd.add sub diff --git a/tests/test_ui.toit b/tests/test_ui.toit index 67b5b15..8fa2218 100644 --- a/tests/test_ui.toit +++ b/tests/test_ui.toit @@ -14,13 +14,13 @@ class TestUi implements cli.Ui: abort: throw "abort" -expect_abort expected/string [block]: +expect-abort expected/string [block]: ui := TestUi exception := catch: block.call ui - expect_equals "abort" exception - all_output := ui.messages.join "\n" - if not all_output.starts_with "Error: $expected": + expect-equals "abort" exception + all-output := ui.messages.join "\n" + if not all-output.starts-with "Error: $expected": print "Expected: $expected" - print "Actual: $all_output" - throw "Expected error message to start with 'Error: $expected'. Actual: $all_output" + print "Actual: $all-output" + throw "Expected error message to start with 'Error: $expected'. Actual: $all-output"