Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow unrecognized options to be ignored #114

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions src/parsing.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions src/settings.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()) :
Expand All @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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)")
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions test/argparse_test02.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 36 additions & 5 deletions test/argparse_test03.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -127,6 +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"])
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)
Expand Down Expand Up @@ -163,4 +190,8 @@ let s = ap_settings3()

end

for (s, ignore) = [(ap_settings3(), false), (ap_settings3b(), true)]
runtest(s, ignore)
end

end
19 changes: 19 additions & 0 deletions test/argparse_test13.jl
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down