From 128e51d18b3e6a83e29ced40504bc3234d69b2e6 Mon Sep 17 00:00:00 2001 From: Carlo Baldassi Date: Wed, 22 Jan 2020 18:08:41 +0100 Subject: [PATCH] Add aliases for command arguments Fix #59 --- README.md | 1 + docs/src/arg_table.md | 24 +++++-- docs/src/conflicts.md | 10 +-- examples/argparse_example6.jl | 2 +- src/parsing.jl | 26 ++++++- src/settings.jl | 124 +++++++++++++++++++++++++++------- test/argparse_test05.jl | 47 ++++++++++--- 7 files changed, 185 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index eaf26be..ab780b5 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ See also the examples in the [examples directory](examples). * Drop support for Julia versions v0.6/v0.7 * Parsing does not exit julia by default when in interactive mode now * Added mutually-exclusive and/or required argument groups +* Added command aliases * Renamed function `import_settings` → `import_settings!` ## Changes in release 0.6.2 diff --git a/docs/src/arg_table.md b/docs/src/arg_table.md index b411f43..ff6a1ef 100644 --- a/docs/src/arg_table.md +++ b/docs/src/arg_table.md @@ -274,6 +274,7 @@ using ArgParse Commands are a special kind of arguments which introduce sub-parsing sessions as soon as they are encountered by `parse_args` (and are therefore mutually exclusive). The `ArgParse` module allows commands to look both as positional arguments or as options, with minor differences between the two. +Unlike actual positional arguments, commands that *look* like positional arguments can have extra names (aliases). Commands are introduced by the `action = :command` setting in the argument table. Suppose we save the following script in a file called `cmd_example.jl`: @@ -285,10 +286,10 @@ function parse_commandline() s = ArgParseSettings() @add_arg_table s begin - "cmd1" + "cmd1", "C" help = "first command" action = :command - "cmd2" + "cmd2", "K" help = "second command" action = :command end @@ -307,8 +308,8 @@ $ julia cmd_example.jl --help usage: cmd_example.jl [-h] {cmd1|cmd2} commands: - cmd1 first command - cmd2 second command + cmd1 first command (aliases: C) + cmd2 second command (aliases: K) optional arguments: -h, --help show this help message and exit @@ -325,6 +326,14 @@ Dict("%COMMAND%"=>"cmd1", "cmd1"=>Dict()) This is unless `parse_args` is invoked with `as_symbols=true`, in which case the special key becomes `:_COMMAND_`. (In that case, no other argument is allowed to use `_COMMAND_` as its `dest_name`, or an error will be raised.) +Aliases are recognized when parsing, but the returned `Dict` will always use the command's name (the first entry in the +table): + +```text +$ julia cmd_example.jl C +Dict("%COMMAND%"=>"cmd1", "cmd1"=>Dict()) +``` + Since commands introduce sub-parsing sessions, an additional key will be added for the called command (`"cmd1"` in this case) whose associated value is another `Dict{String, Any}` containing the result of the sub-parsing (in the above case it's empty). In fact, with the default settings, commands have their own help screens: @@ -352,8 +361,8 @@ By default, if commands exist, they are required; this can be avoided by setting The only meaningful settings for commands in an argument entry besides `action` are `help`, `force_override`, `group` and (for flags only) `dest_name`. -The only differences between positional-arguments-like and option-like commands are in the way they are parsed, the fact that options -accept a `dest_name` setting, and that options can have multiple names (e.g. a long and short form). +The only differences between positional-arguments-like and option-like commands are in the way they are parsed, and the fact +that options accept a `dest_name` setting. Note that short-form option-like commands will be still be recognized in the middle of a short options group and trigger a sub-parsing session: for example, if an option `-c` is associated to a command, then `-xch` will parse option `-x` according to the parent @@ -381,6 +390,9 @@ parse_args(["--help"], settings) ``` It is possible to partition the arguments differently by defining and using customized argument groups. +Groups of options can also be declared to be mutually exclusive, meaning that no more than one of the +options in the group can be provided. A group can also be declared to be required, meaning that at least +one argument in the group needs to be provided. ```@docs add_arg_group diff --git a/docs/src/conflicts.md b/docs/src/conflicts.md index c9bf59a..189573a 100644 --- a/docs/src/conflicts.md +++ b/docs/src/conflicts.md @@ -8,7 +8,9 @@ Conflicts between arguments, be them options, positional arguments or commands, `:append_arg`) * Two positional arguments have the same metavar (and are therefore indistinguishable in the usage and help screens and in error messages) -* An argument and a command, or two commands, have the same destination key. +* An argument's destination key is the same as a command name +* Two commands with the same name are given, but one has a long-option form (e.g. `test` and `--test`) +* A command alias is equal to another command's name or alias When the general setting `error_on_conflict` is `true`, or any time the specific `force_override` table entry setting is `false`, any of the above conditions leads to an error. @@ -21,6 +23,6 @@ the resolution of most of the conflicts in favor of the newest added entry. The * In case of duplicate destination key and incompatible types or actions, the older argument is deleted * In case of duplicate positional arguments metavars, the older argument is deleted * A command can override an argument with the same destination key -* However, an argument can never override a command if they have the same destination key; neither can - a command override another command when added with `@add_arg_table` (compatible commands are merged - by [`import_settings`](@ref) though) +* However, an argument can never override a command; neither can a command override another command when added + with `@add_arg_table` (compatible commands are merged by [`import_settings`](@ref) though) +* Conflicting command aliases are removed diff --git a/examples/argparse_example6.jl b/examples/argparse_example6.jl index af0863b..821dde1 100644 --- a/examples/argparse_example6.jl +++ b/examples/argparse_example6.jl @@ -11,7 +11,7 @@ function main(args) "run" action = :command # adds a command which will be read from an argument help = "start running mode" - "jump" + "jump", "ju", "J" # another one, this one has two aliases action = :command help = "start jumping mode" end diff --git a/src/parsing.jl b/src/parsing.jl index 7f766ba..823af3c 100644 --- a/src/parsing.jl +++ b/src/parsing.jl @@ -259,6 +259,7 @@ function gen_help_text(arg::ArgParseField, settings::ArgParseSettings) type_str = "" default_str = "" const_str = "" + alias_str = "" if !is_command_action(arg.action) if arg.arg_type ≠ Any && !(arg.arg_type <: AbstractString) type_str = pre * "(type: " * string_compact(arg.arg_type) @@ -271,9 +272,14 @@ function gen_help_text(arg::ArgParseField, settings::ArgParseSettings) mid = isempty(type_str) && isempty(default_str) ? " (" : ", " const_str = mid * "without arg: " * string_compact(arg.constant) end + else + is_arg(arg) || found_a_bug() + if !isempty(arg.cmd_aliases) + alias_str = pre * "(aliases: " * join(arg.cmd_aliases, ", ") + end end - post = all(isempty, (type_str, default_str, const_str)) ? "" : ")" - return arg.help * type_str * default_str * const_str * post + post = all(isempty, (type_str, default_str, const_str, alias_str)) ? "" : ")" + return arg.help * type_str * default_str * const_str * alias_str * post end function print_group(io::IO, lst::Vector, desc::AbstractString, lc_usable_len::Int, lc_len::Int, @@ -782,6 +788,22 @@ function parse1_optarg(state::ParserState, settings::ArgParseSettings, f::ArgPar elseif f.action == :append_arg push!(out_dict[f.dest_name], opt_arg) elseif f.action == :command_arg + if !haskey(settings, opt_arg) + found = false + for f1 in settings.args_table.fields + (is_cmd(f1) && is_arg(f1)) || continue + for al in f1.cmd_aliases + if opt_arg == al + found = true + opt_arg = f1.constant + break + end + end + found && break + end + !found && argparse_error("unknown command: $opt_arg") + haskey(settings, opt_arg) || found_a_bug() + end out_dict[f.dest_name] = opt_arg command = opt_arg else diff --git a/src/settings.jl b/src/settings.jl index 4004d80..3a1faf9 100644 --- a/src/settings.jl +++ b/src/settings.jl @@ -82,12 +82,13 @@ mutable struct ArgParseField required::Bool eval_arg::Bool help::AbstractString - metavar::Union{AbstractString,Vector} + metavar::Union{AbstractString,Vector{<:AbstractString}} + cmd_aliases::Vector{AbstractString} group::AbstractString fake::Bool ArgParseField() = new("", AbstractString[], AbstractString[], Any, :store_true, - ArgConsumerType(), nothing, nothing, _->true, false, false, "", "", "", - false) + ArgConsumerType(), nothing, nothing, _->true, false, false, "", "", + AbstractString[], "", false) end is_flag(arg::ArgParseField) = is_flag_action(arg.action) @@ -307,9 +308,19 @@ setindex!(s::ArgParseSettings, x::ArgParseSettings, c::AbstractString) = function check_name_format(name::ArgName) isempty(name) && error("empty name") name isa Vector || return true + allopts = true + allargs = true for n in name - isempty(n) && error("empty name") - startswith(n, '-') || error("only options can have multiple names") + isempty(n) && error("empty name") + if startswith(n, '-') + allargs = false + else + allopts = false + end + end + !(allargs || allopts) && error("multiple names must be either all options or all non-options") + for i1 = 1:length(name), i2 = i1+1:length(name) + name[i1] == name[i2] && error("duplicate name $(name[i1])") end return true end @@ -371,6 +382,14 @@ function check_arg_name(name::AbstractString) return true end +function check_cmd_name(name::AbstractString) + isempty(name) && found_a_bug() + startswith(name, '-') && found_a_bug() + occursin(r"\s", name) && error("invalid command name: $name (contains whitespace)") + occursin(r"^%[A-Z]*%$", name) && error("invalid command name: $name (is reserved)") + return true +end + function check_dest_name(name::AbstractString) occursin(r"^%[A-Z]*%$", name) && error("invalid dest_name: $name (is reserved)") return true @@ -403,7 +422,7 @@ end function check_conflicts_with_commands(settings::ArgParseSettings, new_arg::ArgParseField, allow_future_merge::Bool) - for (cmd, ss) in settings.args_table.subsettings + for cmd in keys(settings.args_table.subsettings) cmd == new_arg.dest_name && error("$(idstring(new_arg)) has the same destination of a command: $cmd") end @@ -416,10 +435,16 @@ function check_conflicts_with_commands(settings::ArgParseSettings, for s1 in a.short_opt_name, s2 in new_arg.short_opt_name s1 == s2 && error("short opt name -$(s1) already in use by command $(a.constant)") end - elseif is_cmd(a) && is_cmd(new_arg) && a.constant == new_arg.constant - allow_future_merge || error("command $(a.constant) already in use") - ((is_arg(a) && !is_arg(new_arg)) || (!is_arg(a) && is_arg(new_arg))) && - error("$(idstring(a)) and $(idstring(new_arg)) are incompatible") + elseif is_cmd(a) && is_cmd(new_arg) + if a.constant == new_arg.constant + allow_future_merge || error("command $(a.constant) already in use") + is_arg(a) ≠ is_arg(new_arg) && + error("$(idstring(a)) and $(idstring(new_arg)) are incompatible") + else + for al in new_arg.cmd_aliases + al == a.constant && error("invalid alias $al, command already in use") + end + end end end return true @@ -444,6 +469,19 @@ function check_for_duplicates(args::Vector{ArgParseField}, new_arg::ArgParseFiel if is_arg(a) && is_arg(new_arg) && a.metavar == new_arg.metavar error("two arguments have the same metavar: $(a.metavar)") end + if is_cmd(a) && is_cmd(new_arg) + for al1 in a.cmd_aliases, al2 in new_arg.cmd_aliases + al1 == al2 && error("both commands $(a.constant) and $(new_arg.constant) use the " * + "same alias $al1") + end + for al1 in a.cmd_aliases + al1 == new_arg.constant && error("$al1 already in use as an alias command " * + "$(a.constant)") + end + for al2 in new_arg.cmd_aliases + al2 == a.constant && error("invalid alias $al2, command already in use") + end + end if a.dest_name == new_arg.dest_name a.arg_type == new_arg.arg_type || error("$(idstring(a)) and $(idstring(new_arg)) have the same destination " * @@ -564,8 +602,9 @@ function name_to_fieldnames(name::ArgName, settings::ArgParseSettings) pos_arg = "" long_opts = AbstractString[] short_opts = AbstractString[] + aliases = AbstractString[] r(n) = settings.autofix_names ? replace(n, '_' => '-') : n - function do_one(n; args_allowed = false) + function do_one(n, cmd_check = true) if startswith(n, "--") n == "--" && error("illegal option name: --") long_opt_name = r(n[3:end]) @@ -577,18 +616,25 @@ function name_to_fieldnames(name::ArgName, settings::ArgParseSettings) check_short_opt_name(short_opt_name, settings) push!(short_opts, short_opt_name) else - args_allowed || found_a_bug() - check_arg_name(n) - pos_arg = n + if cmd_check + check_cmd_name(n) + else + check_arg_name(n) + end + if isempty(pos_arg) + pos_arg = n + else + push!(aliases, n) + end end end if name isa Vector foreach(do_one, name) else - do_one(name, args_allowed = true) + do_one(name, false) end - return pos_arg, long_opts, short_opts + return pos_arg, long_opts, short_opts, aliases end function auto_dest_name(pos_arg::AbstractString, @@ -670,7 +716,8 @@ The `table` is a list in which each element can be either `String`, or a tuple o `String`, or an assigmment expression, or a block: * a `String`, a tuple or a vector introduces a new positional argument or option. Tuples and vectors - are only allowed for options and provide alternative names (e.g. `["--opt", "-o"]`) + are only allowed for options or commands, and provide alternative names (e.g. `["--opt", "-o"]` or + `["checkout", "co"]`) * assignment expressions (i.e. expressions using `=`, `:=` or `=>`) describe the previous argument behavior (e.g. `help = "an option"` or `required => false`). See the [Argument entry settings](@ref) section for a complete description @@ -855,7 +902,9 @@ function add_arg_field(settings::ArgParseSettings, name::ArgName; desc...) nargs isa ArgConsumerType || (nargs = ArgConsumerType(nargs)) action isa Symbol || (action = Symbol(action)) - is_opt = name isa Vector || startswith(name, '-') + is_opt = name isa Vector ? + startswith(first(name), '-') : + startswith(name, '-') check_action_is_valid(action) @@ -873,12 +922,17 @@ function add_arg_field(settings::ArgParseSettings, name::ArgName; desc...) metavar isa Vector && error("multiple metavars only supported for optional arguments") end - pos_arg, long_opts, short_opts = name_to_fieldnames(name, settings) + pos_arg, long_opts, short_opts, cmd_aliases = name_to_fieldnames(name, settings) + + if !isempty(cmd_aliases) + is_command_action(action) || error("only command arguments can have multiple names (aliases)") + end new_arg.dest_name = auto_dest_name(pos_arg, long_opts, short_opts, settings.autofix_names) new_arg.long_opt_name = long_opts new_arg.short_opt_name = short_opts + new_arg.cmd_aliases = cmd_aliases new_arg.nargs = nargs new_arg.action = action @@ -1186,8 +1240,7 @@ function override_conflicts_with_commands(settings::ArgParseSettings, new_cmd::A end function override_duplicates(args::Vector{ArgParseField}, new_arg::ArgParseField) ids0 = Int[] - for ia in 1:length(args) - a = args[ia] + for (ia,a) in enumerate(args) if (a.dest_name == new_arg.dest_name) && ((a.arg_type ≠ new_arg.arg_type) || (is_multi_action(a.action) && !is_multi_action(new_arg.action)) || @@ -1196,19 +1249,37 @@ function override_duplicates(args::Vector{ArgParseField}, new_arg::ArgParseField push!(ids0, ia) continue end - if is_arg(a) && is_arg(new_arg) && a.metavar == new_arg.metavar + if is_arg(a) && is_arg(new_arg) && !(is_cmd(a) && is_cmd(new_arg)) && + a.metavar == new_arg.metavar # unsolvable conflict, mark for deletion push!(ids0, ia) continue end + # delete conflicting command aliases for different commands + if is_cmd(a) && is_cmd(new_arg) && a.constant ≠ new_arg.constant + ids = Int[] + for (ial1, al1) in enumerate(a.cmd_aliases) + if al1 == new_arg.constant + push!(ids, ial1) + else + for al2 in new_arg.cmd_aliases + al1 == al2 && push!(ids, ial1) + end + end + end + while !isempty(ids) + splice!(a.cmd_aliases, pop!(ids)) + end + end + if is_arg(a) || is_arg(new_arg) # not an option, skip continue end if is_cmd(a) && is_cmd(new_arg) && a.constant == new_arg.constant && !is_arg(a) - @assert !is_arg(new_arg) # this is ensured by check_settings_are_compatible + is_arg(new_arg) && found_a_bug() # this is ensured by check_settings_are_compatible # two command flags with the same command -> should have already been taken care of, # by either check_settings_are_compatible or merge_commands continue @@ -1265,14 +1336,17 @@ function merge_commands(fields::Vector{ArgParseField}, ofields::Vector{ArgParseF oids = Int[] for a in fields, ioa = 1:length(ofields) oa = ofields[ioa] - if is_cmd(a) && is_cmd(oa) && a.constant == oa.constant && !is_arg(a) - @assert !is_arg(oa) # this is ensured by check_settings_are_compatible + if is_cmd(a) && is_cmd(oa) && a.constant == oa.constant + is_arg(a) ≠ is_arg(oa) && found_a_bug() # ensured by check_settings_are_compatible for l in oa.long_opt_name l ∈ a.long_opt_name || push!(a.long_opt_name, l) end for s in oa.short_opt_name s ∈ a.short_opt_name || push!(a.short_opt_name, s) end + for al in oa.cmd_aliases + al ∈ a.cmd_aliases || push!(a.cmd_aliases, al) + end a.group = oa.group # note: the group may not be present yet, but it will be # added later push!(oids, ioa) diff --git a/test/argparse_test05.jl b/test/argparse_test05.jl index e811778..594fa45 100644 --- a/test/argparse_test05.jl +++ b/test/argparse_test05.jl @@ -12,7 +12,7 @@ function ap_settings5() "run" action = :command help = "start running mode" - "jump" + "jump", "ju", "J" action = :command help = "start jumping mode" end @@ -68,7 +68,7 @@ let s = ap_settings5() commands: run start running mode - jump start jumping mode + jump start jumping mode (aliases: ju, J) """ @@ -113,6 +113,8 @@ let s = ap_settings5() @noout_test ap_test5(["jump", "--help"]) ≡ nothing @test ap_test5(["jump"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>false, "%COMMAND%"=>nothing)) @test ap_test5(["jump", "--higher", "--clap"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>true, "%COMMAND%"=>"clap_feet", "clap_feet"=>Dict{String,Any}())) + @test ap_test5(["ju", "--higher", "--clap"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>true, "%COMMAND%"=>"clap_feet", "clap_feet"=>Dict{String,Any}())) + @test ap_test5(["J", "--higher", "--clap"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>true, "%COMMAND%"=>"clap_feet", "clap_feet"=>Dict{String,Any}())) @noout_test ap_test5(["jump", "--higher", "--clap", "--help"]) ≡ nothing @noout_test ap_test5(["jump", "--higher", "--clap", "--help"], as_symbols = true) ≡ nothing @ap_test_throws ap_test5(["jump", "--clap", "--higher"]) @@ -122,11 +124,14 @@ let s = ap_settings5() @test ap_test5(["jump", "-sbt"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>false, "%COMMAND%"=>"som", "som"=>Dict{String,Any}("t"=>1, "b"=>true))) @test ap_test5(["jump", "-s", "-t2"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>false, "%COMMAND%"=>"som", "som"=>Dict{String,Any}("t"=>2, "b"=>false))) @test ap_test5(["jump", "-sbt2"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>false, "%COMMAND%"=>"som", "som"=>Dict{String,Any}("t"=>2, "b"=>true))) + @test ap_test5(["ju", "-sbt2"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>false, "%COMMAND%"=>"som", "som"=>Dict{String,Any}("t"=>2, "b"=>true))) + @test ap_test5(["J", "-sbt2"]) == Dict{String,Any}("%COMMAND%"=>"jump", "jump"=>Dict{String,Any}("higher"=>false, "%COMMAND%"=>"som", "som"=>Dict{String,Any}("t"=>2, "b"=>true))) @noout_test ap_test5(["jump", "-sbht2"]) ≡ nothing @ap_test_throws ap_test5(["jump", "-st2b"]) @ap_test_throws ap_test5(["jump", "-stb"]) @ap_test_throws ap_test5(["jump", "-sb-"]) @ap_test_throws ap_test5(["jump", "-s-b"]) + @ap_test_throws ap_test5(["ju", "-s-b"]) @test ap_test5(["run", "--speed", "3"], as_symbols = true) == Dict{Symbol,Any}(:_COMMAND_=>:run, :run=>Dict{Symbol,Any}(:speed=>3.0)) @@ -139,6 +144,21 @@ let s = ap_settings5() # same dest_name as command @ee_test_throws @add_arg_table(s["jump"], "--som") @ee_test_throws @add_arg_table(s["jump"], "-s", dest_name = "som") + # same name as command alias + @ee_test_throws @add_arg_table(s, "ju") + @ee_test_throws @add_arg_table(s, "J") + # new command with the same name as another one + @ee_test_throws @add_arg_table(s, ["run", "R"], action = :command) + @ee_test_throws @add_arg_table(s, "jump", action = :command) + # new command with the same name as another one's alias + @ee_test_throws @add_arg_table(s, "ju", action = :command) + @ee_test_throws @add_arg_table(s, "J", action = :command) + # new command with an alias which is the same as another command + @ee_test_throws @add_arg_table(s, ["fast", "run"], action = :command) + @ee_test_throws @add_arg_table(s, ["R", "jump"], action = :command) + # new command with an alias which is already in use + @ee_test_throws @add_arg_table(s, ["R", "ju"], action = :command) + @ee_test_throws @add_arg_table(s, ["R", "S", "J"], action = :command) # conflict between dest_name and a reserved Symbol @add_arg_table(s, "--COMMAND", dest_name="_COMMAND_") @@ -154,10 +174,10 @@ function ap_settings5b() exit_after_help = false) @add_arg_table s0 begin - "run" + "run", "R" action = :command help = "start running mode" - "jump" + "jump", "ju" action = :command help = "start jumping mode" "--time" @@ -208,10 +228,10 @@ function ap_settings5b() s0["jump"]["som"].description = "Somersault jump mode" @add_arg_table s begin - "jump" + "jump", "run", "J" # The "run" alias will be overridden action = :command help = "start jumping mode" - "fly" + "fly", "R" # The "R" alias will be overridden action = :command help = "start flying mode" # next opt will be overridden (same dest_name as s0's --time, @@ -268,20 +288,20 @@ let s = ap_settings5b() ap_test5b(args) = parse_args(args, s) @test stringhelp(s) == """ - usage: $(basename(Base.source_path())) [--time T] {fly|run|jump} + usage: $(basename(Base.source_path())) [--time T] {jump|fly|run} commands: + jump start jumping mode (aliases: J, ju) fly start flying mode - run start running mode - jump start jumping mode + run start running mode (aliases: R) optional arguments: --time T time at which to perform the command (default: "now") """ - stringhelp(s["jump"]) == """ - usage: argparse_test5.jl jump [--lower] [--higher] [--help] + @test stringhelp(s["jump"]) == """ + usage: $(basename(Base.source_path())) jump [--lower] [--higher] [--help] {-c|--somersault} commands: @@ -301,10 +321,15 @@ let s = ap_settings5b() @test ap_test5b(["fly"]) == Dict{String,Any}("%COMMAND%"=>"fly", "time"=>"now", "fly"=>Dict{String,Any}("glade"=>false)) @test ap_test5b(["jump", "--lower", "--clap"]) == Dict{String,Any}("%COMMAND%"=>"jump", "time"=>"now", "jump"=>Dict{String,Any}("%COMMAND%"=>"clap_feet", "higher"=>false, "clap_feet"=>Dict{String,Any}("whistle"=>false))) + @test ap_test5b(["ju", "--lower", "--clap"]) == Dict{String,Any}("%COMMAND%"=>"jump", "time"=>"now", + "jump"=>Dict{String,Any}("%COMMAND%"=>"clap_feet", "higher"=>false, "clap_feet"=>Dict{String,Any}("whistle"=>false))) + @test ap_test5b(["J", "--lower", "--clap"]) == Dict{String,Any}("%COMMAND%"=>"jump", "time"=>"now", + "jump"=>Dict{String,Any}("%COMMAND%"=>"clap_feet", "higher"=>false, "clap_feet"=>Dict{String,Any}("whistle"=>false))) @noout_test ap_test5b(["jump", "--lower", "--help"]) ≡ nothing @noout_test ap_test5b(["jump", "--lower", "--clap", "--version"]) ≡ nothing @ap_test_throws ap_test5b(["jump"]) @test ap_test5b(["run", "--speed=3"]) == Dict{String,Any}("%COMMAND%"=>"run", "time"=>"now", "run"=>Dict{String,Any}("speed"=>3.0)) + @test ap_test5b(["R", "--speed=3"]) == Dict{String,Any}("%COMMAND%"=>"run", "time"=>"now", "run"=>Dict{String,Any}("speed"=>3.0)) end end