From 848421232b0369bf64e47676b9347961252d0477 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Fri, 1 Oct 2021 17:53:53 +0100 Subject: [PATCH 1/2] Add tests for error when unrecognized option is passed --- test/argparse_test02.jl | 4 ++++ test/argparse_test03.jl | 3 +++ 2 files changed, 7 insertions(+) diff --git a/test/argparse_test02.jl b/test/argparse_test02.jl index 6d45f3f..3823e10 100644 --- a/test/argparse_test02.jl +++ b/test/argparse_test02.jl @@ -262,6 +262,10 @@ for s = [ap_settings2(), ap_settings2b(), ap_settings2c(), ap_settings2d(), ap_s @ap_test_throws ap_test2(["--opt="]) @ap_test_throws ap_test2(["--opt", "", "X", "Y"]) @ap_test_throws ap_test2(["--opt", "1e-2", "X", "Y"]) + @ap_test_throws ap_test2(["X", "Y", "-z"]) + @ap_test_throws ap_test2(["X", "Y", "-z", "a b c"]) + @ap_test_throws ap_test2(["X", "Y", "--zzz"]) + @ap_test_throws ap_test2(["X", "Y", "--zzz", "a b c"]) @ee_test_throws @add_arg_table!(s, "required_arg_after_optional_args", required = true) # wrong default diff --git a/test/argparse_test03.jl b/test/argparse_test03.jl index c222558..d6dbcb9 100644 --- a/test/argparse_test03.jl +++ b/test/argparse_test03.jl @@ -127,6 +127,9 @@ let s = ap_settings3() @ap_test_throws ap_test3(["--custom", "default"]) @ap_test_throws ap_test3(["--oddint", "0"]) @ap_test_throws ap_test3(["--collect", "0.5"]) + @ap_test_throws ap_test3(["--foobar"]) + @ap_test_throws ap_test3(["--foobar", "1"]) + @ap_test_throws ap_test3(["--foobar", "a b c", "--opt1", "--awk", "X", "X", "--opt2", "--opt2", "-k", "--coll", "5", "-u", "--array=[4]", "--custom", "custom", "--collect", "3", "--awkward-option=Y", "X", "--opt1", "--oddint", "-1"]) # invalid option name @ee_test_throws @add_arg_table!(s, "-2", action = :store_true) From 5d7c2471941395495a9ff22b568d187358bb0e4a Mon Sep 17 00:00:00 2001 From: John Omotani Date: Fri, 1 Oct 2021 17:56:27 +0100 Subject: [PATCH 2/2] Setting to skip unrecognized options Only works when there are no positional arguments. Positional arguments would lead to possible ambiguity over what to do with arguments after ignored option. --- src/parsing.jl | 33 +++++++++++++++++++++++++++++-- src/settings.jl | 18 +++++++++++++++-- test/argparse_test03.jl | 44 +++++++++++++++++++++++++++++++++-------- test/argparse_test13.jl | 19 ++++++++++++++++++ test/runtests.jl | 2 +- 5 files changed, 103 insertions(+), 13 deletions(-) create mode 100644 test/argparse_test13.jl diff --git a/src/parsing.jl b/src/parsing.jl index e868896..38ee8b5 100644 --- a/src/parsing.jl +++ b/src/parsing.jl @@ -814,6 +814,19 @@ function parse1_optarg!(state::ParserState, settings::ArgParseSettings, f::ArgPa return end +# Ignore arguments until next option is reached +function skip_to_next_opt!(state::ParserState, settings::ArgParseSettings) + while !isempty(state.args_list) + if looks_like_an_option(state.args_list[1], settings) + break + end + popfirst!(state.args_list) + end + state.arg_consumed = true + state.command = nothing + return +end + # parse long opts function parse_long_opt!(state::ParserState, settings::ArgParseSettings) opt_name = state.token @@ -838,7 +851,15 @@ function parse_long_opt!(state::ParserState, settings::ArgParseSettings) end exact_match && break end - nfound == 0 && argparse_error("unrecognized option --$opt_name") + if nfound == 0 + if settings.ignore_unrecognized_opts + # Consume and skip any arguments after the option + skip_to_next_opt!(state, settings) + return + else + argparse_error("unrecognized option --$opt_name") + end + end nfound > 1 && argparse_error("long option --$opt_name is ambiguous ($nfound partial matches)") opt_name = fln @@ -881,7 +902,15 @@ function parse_short_opt!(state::ParserState, settings::ArgParseSettings) found |= any(sn->sn==opt_name, f.short_opt_name) found && break end - found || argparse_error("unrecognized option -$opt_name") + if !found + if settings.ignore_unrecognized_opts + # Consume and skip any arguments after the option + skip_to_next_opt!(state, settings) + break + else + argparse_error("unrecognized option -$opt_name") + end + end if is_flag(f) parse1_flag!(state, settings, f, next_is_eq, "-"*opt_name) else diff --git a/src/settings.jl b/src/settings.jl index 4d4c979..7bc87a0 100644 --- a/src/settings.jl +++ b/src/settings.jl @@ -184,6 +184,10 @@ This is the list of general settings currently available: invoked from a script or in an interactive environment (e.g. REPL/IJulia). In non-interactive (script) mode, it calls `ArgParse.cmdline_handler`, which prints the error text and the usage screen on standard error and exits Julia with error code 1: +* `ignore_unrecognized_opts` (default = `false`): if `true`, unrecognized options will be skipped. + `ignore_unrecognized_opts=true` cannot be used when there are any positional arguments, because it + is potentially ambiguous whether values after the unrecoginized option are supposed to be handled + by the option or by the positional argument. ```julia function cmdline_handler(settings::ArgParseSettings, err, err_code::Int = 1) @@ -252,6 +256,7 @@ mutable struct ArgParseSettings preformatted_description::Bool preformatted_epilog::Bool exit_after_help::Bool + ignore_unrecognized_opts::Bool function ArgParseSettings(;prog::AbstractString = Base.source_path() ≢ nothing ? basename(Base.source_path()) : @@ -271,7 +276,8 @@ mutable struct ArgParseSettings exc_handler::Function = default_handler, preformatted_description::Bool = false, preformatted_epilog::Bool = false, - exit_after_help::Bool = !isinteractive() + exit_after_help::Bool = !isinteractive(), + ignore_unrecognized_opts::Bool = false ) fromfile_prefix_chars = check_prefix_chars(fromfile_prefix_chars) return new( @@ -280,7 +286,7 @@ mutable struct ArgParseSettings suppress_warnings, allow_ambiguous_opts, commands_are_required, copy(std_groups), "", ArgParseTable(), exc_handler, preformatted_description, preformatted_epilog, - exit_after_help + exit_after_help, ignore_unrecognized_opts ) end end @@ -356,6 +362,12 @@ function check_nargs_and_action(nargs::ArgConsumerType, action::Symbol) return true end +function check_ignore_unrecognized_opts(settings::ArgParseSettings, is_opt::Bool) + !is_opt && settings.ignore_unrecognized_opts && + error("cannot use ignore_unrecognized_opts=true with positional arguments") + return true +end + function check_long_opt_name(name::AbstractString, settings::ArgParseSettings) '=' ∈ name && error("illegal option name: $name (contains '=')") occursin(r"\s", name) && error("illegal option name: $name (contains whitespace)") @@ -920,6 +932,8 @@ function add_arg_field!(settings::ArgParseSettings, name::ArgName; desc...) check_nargs_and_action(nargs, action) + check_ignore_unrecognized_opts(settings, is_opt) + new_arg = ArgParseField() is_flag = is_flag_action(action) diff --git a/test/argparse_test03.jl b/test/argparse_test03.jl index d6dbcb9..b2bf04d 100644 --- a/test/argparse_test03.jl +++ b/test/argparse_test03.jl @@ -14,10 +14,7 @@ end Base.show(io::IO, ::Type{CustomType}) = print(io, "CustomType") Base.show(io::IO, c::CustomType) = print(io, "CustomType()") -function ap_settings3() - - s = ArgParseSettings("Test 3 for ArgParse.jl", - exc_handler = ArgParse.debug_handler) +function ap_add_table3!(s::ArgParseSettings) @add_arg_table! s begin "--opt1" @@ -72,11 +69,30 @@ function ap_settings3() help = "either X or Y; all XY's are " * "stored in chunks" end +end + +function ap_settings3() + + s = ArgParseSettings("Test 3 for ArgParse.jl", + exc_handler = ArgParse.debug_handler) + + ap_add_table3!(s) return s end -let s = ap_settings3() +function ap_settings3b() + + s = ArgParseSettings("Test 3 for ArgParse.jl", + exc_handler = ArgParse.debug_handler, + ignore_unrecognized_opts = true) + + ap_add_table3!(s) + + return s +end + +function runtest(s, ignore) ap_test3(args) = parse_args(args, s) ## ugly workaround for the change of printing Vectors in julia 1.6, @@ -127,9 +143,17 @@ let s = ap_settings3() @ap_test_throws ap_test3(["--custom", "default"]) @ap_test_throws ap_test3(["--oddint", "0"]) @ap_test_throws ap_test3(["--collect", "0.5"]) - @ap_test_throws ap_test3(["--foobar"]) - @ap_test_throws ap_test3(["--foobar", "1"]) - @ap_test_throws ap_test3(["--foobar", "a b c", "--opt1", "--awk", "X", "X", "--opt2", "--opt2", "-k", "--coll", "5", "-u", "--array=[4]", "--custom", "custom", "--collect", "3", "--awkward-option=Y", "X", "--opt1", "--oddint", "-1"]) + if ignore + ap_test3(["--foobar"]) + @test ap_test3(["--foobar"]) == Dict{String,Any}("O_stack"=>String[], "k"=>0, "u"=>0, "array"=>[7, 3, 2], "custom"=>CustomType(), "oddint"=>1, "collect"=>[], "awk"=>Any[Any["X"]]) + @test ap_test3(["--foobar", "1"]) == Dict{String,Any}("O_stack"=>String[], "k"=>0, "u"=>0, "array"=>[7, 3, 2], "custom"=>CustomType(), "oddint"=>1, "collect"=>[], "awk"=>Any[Any["X"]]) + @test ap_test3(["--foobar", "a b c", "--opt1", "--awk", "X", "X", "--opt2", "--opt2", "-k", "--coll", "5", "-u", "--array=[4]", "--custom", "custom", "--collect", "3", "--awkward-option=Y", "X", "--opt1", "--oddint", "-1"]) == + Dict{String,Any}("O_stack"=>String["O1", "O2", "O2", "O1"], "k"=>42, "u"=>42.0, "array"=>[4], "custom"=>CustomType(), "oddint"=>-1, "collect"=>[5, 3], "awk"=>Any[Any["X"], Any["X", "X"], Any["Y", "X"]]) + else + @ap_test_throws ap_test3(["--foobar"]) + @ap_test_throws ap_test3(["--foobar", "1"]) + @ap_test_throws ap_test3(["--foobar", "a b c", "--opt1", "--awk", "X", "X", "--opt2", "--opt2", "-k", "--coll", "5", "-u", "--array=[4]", "--custom", "custom", "--collect", "3", "--awkward-option=Y", "X", "--opt1", "--oddint", "-1"]) + end # invalid option name @ee_test_throws @add_arg_table!(s, "-2", action = :store_true) @@ -166,4 +190,8 @@ let s = ap_settings3() end +for (s, ignore) = [(ap_settings3(), false), (ap_settings3b(), true)] + runtest(s, ignore) +end + end diff --git a/test/argparse_test13.jl b/test/argparse_test13.jl new file mode 100644 index 0000000..3332ef0 --- /dev/null +++ b/test/argparse_test13.jl @@ -0,0 +1,19 @@ +# test 13: setting ignore_unrecognized_opts with positional arguments is an error + +@testset "test 13" begin +s = ArgParseSettings(description = "Test 13 for ArgParse.jl", + epilog = "Have fun!", + version = "Version 1.0", + add_version = true, + exc_handler = ArgParse.debug_handler, + ignore_unrecognized_opts = true) + +@ee_test_throws @add_arg_table! s begin + "arg1" + nargs = 2 # eats up two arguments; puts the result in a Vector + help = "first argument, two " * + "entries at once" + required = true +end + +end diff --git a/test/runtests.jl b/test/runtests.jl index e19cddf..dbcd279 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,7 +2,7 @@ module ArgParseTests include("common.jl") -for i = 1:12 +for i = 1:13 try s_i = lpad(string(i), 2, "0") include("argparse_test$s_i.jl")