diff --git a/README.md b/README.md index 17849c13c..93ad87612 100644 --- a/README.md +++ b/README.md @@ -1123,6 +1123,24 @@ option_groups. These are: executes after the first argument of an application is processed. See [Subcommand callbacks](#callbacks) for some additional details. - `.allow_extras()`: Do not throw an error if extra arguments are left over. +- `.allow_extras(CLI::ExtrasMode)`: Specify the method of handling unrecognized + arguments. + - `CLI::ExtrasMode::Error`: generate an error on unrecognized argument. Same + as `.allow_extras(false)`. + - `CLI::ExtrasMode::ErrorImmediately`: generate an error immediately on + parsing an unrecognized option`. + - `CLI::ExtrasMode::Ignore`: ignore any unrecognized argument, do not generate + an error. + - `CLI::ExtrasMode::AssumeSingleArgument`: After an unrecognized flag or + option argument, if the following argument is not a flag or option argument + assume it an argument and treat it as also unrecognized even if it would + otherwise go to a positional argument + - `CLI::ExtrasMode::AssumeMultipleArguments`:After an unrecognized flag or + option argument, if the following arguments are not a flag or option + argument assume they are arguments and treat them as also unrecognized even + if it would otherwise go to a positional argument + - `CLI::ExtrasMode::Capture`: capture all unrecognized arguments, same as + `true` for `.allow_extras`: - `.positionals_at_end()`: Specify that positional arguments occur as the last arguments and throw an error if an unexpected positional is encountered. - `.prefix_command()`: Like `allow_extras`, but stop processing immediately on diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 969b061fd..4db2e2baa 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -66,7 +66,20 @@ CLI11_INLINE std::string simple(const App *app, const Error &e); CLI11_INLINE std::string help(const App *app, const Error &e); } // namespace FailureMessage +/// enumeration of modes of how to deal with command line extras +enum class ExtrasMode : std::uint8_t { + Error = 0, + ErrorImmediately, + Ignore, + AssumeSingleArgument, + AssumeMultipleArguments, + Capture +}; + /// enumeration of modes of how to deal with extras in config files +enum class ConfigExtrasMode : std::uint8_t { Error = 0, Ignore, IgnoreAll, Capture }; + +/// @brief enumeration of modes of how to deal with extras in config files enum class config_extras_mode : std::uint8_t { error = 0, ignore, ignore_all, capture }; /// @brief enumeration of prefix command modes, separator requires that the first extra argument be a "--", other @@ -116,11 +129,11 @@ class App { std::string description_{}; /// If true, allow extra arguments (ie, don't throw an error). INHERITABLE - bool allow_extras_{false}; + ExtrasMode allow_extras_{ExtrasMode::Error}; /// If ignore, allow extra arguments in the ini file (ie, don't throw an error). INHERITABLE /// if error, error on an extra argument, and if capture feed it to the app - config_extras_mode allow_config_extras_{config_extras_mode::ignore}; + ConfigExtrasMode allow_config_extras_{ConfigExtrasMode::Ignore}; /// If true, cease processing on an unrecognized option (implies allow_extras) INHERITABLE PrefixCommandMode prefix_command_{PrefixCommandMode::Off}; @@ -388,6 +401,12 @@ class App { /// Remove the error when extras are left over on the command line. App *allow_extras(bool allow = true) { + allow_extras_ = allow ? ExtrasMode::Capture : ExtrasMode::Error; + return this; + } + + /// Remove the error when extras are left over on the command line. + App *allow_extras(ExtrasMode allow) { allow_extras_ = allow; return this; } @@ -461,16 +480,22 @@ class App { /// ignore extras in config files App *allow_config_extras(bool allow = true) { if(allow) { - allow_config_extras_ = config_extras_mode::capture; - allow_extras_ = true; + allow_config_extras_ = ConfigExtrasMode::Capture; + allow_extras_ = ExtrasMode::Capture; } else { - allow_config_extras_ = config_extras_mode::error; + allow_config_extras_ = ConfigExtrasMode::Error; } return this; } /// ignore extras in config files App *allow_config_extras(config_extras_mode mode) { + allow_config_extras_ = static_cast(mode); + return this; + } + + /// ignore extras in config files + App *allow_config_extras(ConfigExtrasMode mode) { allow_config_extras_ = mode; return this; } @@ -1179,7 +1204,10 @@ class App { CLI11_NODISCARD PrefixCommandMode get_prefix_command_mode() const { return prefix_command_; } /// Get the status of allow extras - CLI11_NODISCARD bool get_allow_extras() const { return allow_extras_; } + CLI11_NODISCARD bool get_allow_extras() const { return allow_extras_ > ExtrasMode::Ignore; } + + /// Get the mode of allow_extras + CLI11_NODISCARD ExtrasMode get_allow_extras_mode() const { return allow_extras_; } /// Get the status of required CLI11_NODISCARD bool get_required() const { return required_; } @@ -1210,7 +1238,9 @@ class App { CLI11_NODISCARD bool get_validate_optional_arguments() const { return validate_optional_arguments_; } /// Get the status of allow extras - CLI11_NODISCARD config_extras_mode get_allow_config_extras() const { return allow_config_extras_; } + CLI11_NODISCARD config_extras_mode get_allow_config_extras() const { + return static_cast(allow_config_extras_); + } /// Get a pointer to the help flag. Option *get_help_ptr() { return help_ptr_; } diff --git a/include/CLI/impl/App_inl.hpp b/include/CLI/impl/App_inl.hpp index b894c8c4a..0785e5dd8 100644 --- a/include/CLI/impl/App_inl.hpp +++ b/include/CLI/impl/App_inl.hpp @@ -986,7 +986,7 @@ CLI11_NODISCARD CLI11_INLINE std::vector App::remaining(bool recurs } // Get from a subcommand that may allow extras if(recurse) { - if(!allow_extras_) { + if(allow_extras_ == ExtrasMode::Error || allow_extras_ == ExtrasMode::Ignore) { for(const auto &sub : subcommands_) { if(sub->name_.empty() && !sub->missing_.empty()) { for(const std::pair &miss : sub->missing_) { @@ -1461,13 +1461,13 @@ CLI11_INLINE void App::_process() { } CLI11_INLINE void App::_process_extras() { - if(!allow_extras_ && prefix_command_ == PrefixCommandMode::Off) { + if(allow_extras_ == ExtrasMode::Error && prefix_command_ == PrefixCommandMode::Off) { std::size_t num_left_over = remaining_size(); if(num_left_over > 0) { throw ExtrasError(name_, remaining(false)); } } - if(!allow_extras_ && prefix_command_ == PrefixCommandMode::SeparatorOnly) { + if(allow_extras_ == ExtrasMode::Error && prefix_command_ == PrefixCommandMode::SeparatorOnly) { std::size_t num_left_over = remaining_size(); if(num_left_over > 0) { if(remaining(false).front() != "--") { @@ -1555,7 +1555,7 @@ CLI11_INLINE void App::_parse_stream(std::istream &input) { CLI11_INLINE void App::_parse_config(const std::vector &args) { for(const ConfigItem &item : args) { - if(!_parse_single_config(item) && allow_config_extras_ == config_extras_mode::error) + if(!_parse_single_config(item) && allow_config_extras_ == ConfigExtrasMode::Error) throw ConfigError::Extras(item.fullname()); } } @@ -1730,7 +1730,8 @@ CLI11_INLINE bool App::_parse_single(std::vector &args, bool &posit args.pop_back(); positional_only = true; if(get_prefix_command()) { - _move_to_missing(classifier, "--"); + // don't care about extras mode here + missing_.emplace_back(classifier, "--"); while(!args.empty()) { missing_.emplace_back(detail::Classifier::NONE, args.back()); args.pop_back(); @@ -2153,6 +2154,16 @@ App::_parse_arg(std::vector &args, detail::Classifier current_type, missing_.emplace_back(detail::Classifier::NONE, args.back()); args.pop_back(); } + } else if(allow_extras_ == ExtrasMode::AssumeSingleArgument) { + if(!args.empty() && _recognize(args.back(), false) == detail::Classifier::NONE) { + _move_to_missing(detail::Classifier::NONE, args.back()); + args.pop_back(); + } + } else if(allow_extras_ == ExtrasMode::AssumeMultipleArguments) { + while(!args.empty() && _recognize(args.back(), false) == detail::Classifier::NONE) { + _move_to_missing(detail::Classifier::NONE, args.back()); + args.pop_back(); + } } return true; } @@ -2342,20 +2353,31 @@ CLI11_NODISCARD CLI11_INLINE const std::string &App::_compare_subcommand_names(c return estring; } +inline bool capture_extras(ExtrasMode mode) { + return mode == ExtrasMode::Capture || mode == ExtrasMode::AssumeSingleArgument || + mode == ExtrasMode::AssumeMultipleArguments; +} CLI11_INLINE void App::_move_to_missing(detail::Classifier val_type, const std::string &val) { - if(allow_extras_ || subcommands_.empty() || get_prefix_command()) { - missing_.emplace_back(val_type, val); + if(allow_extras_ == ExtrasMode::ErrorImmediately) { + throw ExtrasError(name_, std::vector{val}); + } + if(capture_extras(allow_extras_) || subcommands_.empty() || get_prefix_command()) { + if(allow_extras_ != ExtrasMode::Ignore) { + missing_.emplace_back(val_type, val); + } return; } // allow extra arguments to be placed in an option group if it is allowed there for(auto &subc : subcommands_) { - if(subc->name_.empty() && subc->allow_extras_) { + if(subc->name_.empty() && capture_extras(subc->allow_extras_)) { subc->missing_.emplace_back(val_type, val); return; } } - // if we haven't found any place to put them yet put them in missing - missing_.emplace_back(val_type, val); + if(allow_extras_ != ExtrasMode::Ignore) { + // if we haven't found any place to put them yet put them in missing + missing_.emplace_back(val_type, val); + } } CLI11_INLINE void App::_move_option(Option *opt, App *app) { diff --git a/tests/AppTest.cpp b/tests/AppTest.cpp index 8aa40cfda..2026b35ca 100644 --- a/tests/AppTest.cpp +++ b/tests/AppTest.cpp @@ -2391,6 +2391,12 @@ TEST_CASE_METHOD(TApp, "AllowExtras", "[app]") { REQUIRE_NOTHROW(run()); CHECK(val); CHECK(std::vector({"-x"}) == app.remaining()); + + app.allow_extras(CLI::ExtrasMode::Ignore); + val = false; + REQUIRE_NOTHROW(run()); + CHECK(val); + CHECK(app.remaining().empty()); } TEST_CASE_METHOD(TApp, "AllowExtrasOrder", "[app]") { @@ -2410,7 +2416,6 @@ TEST_CASE_METHOD(TApp, "AllowExtrasOrder", "[app]") { TEST_CASE_METHOD(TApp, "AllowExtrasCascade", "[app]") { app.allow_extras(); - args = {"-x", "45", "-f", "27"}; REQUIRE_NOTHROW(run()); CHECK(std::vector({"-x", "45", "-f", "27"}) == app.remaining()); @@ -2428,6 +2433,70 @@ TEST_CASE_METHOD(TApp, "AllowExtrasCascade", "[app]") { CHECK(27 == v2); } +TEST_CASE_METHOD(TApp, "AllowExtrasAssumptions", "[app]") { + + app.allow_extras(CLI::ExtrasMode::AssumeSingleArgument); + + std::string one; + std::string two; + app.add_option("--one", one); + app.add_option("two", two); + args = {"--one", "45", "--three", "27", "this"}; + + REQUIRE_NOTHROW(run()); + CHECK(one == "45"); + CHECK(two == "this"); + CHECK(app.remaining().size() == 2U); + + two.clear(); + app.allow_extras(CLI::ExtrasMode::AssumeMultipleArguments); + + run(); + CHECK(one == "45"); + CHECK(two.empty()); + CHECK(app.remaining().size() == 3U); + app.allow_extras(CLI::ExtrasMode::AssumeSingleArgument); + CHECK(app.get_allow_extras_mode() == CLI::ExtrasMode::AssumeSingleArgument); + args = {"--three", "27", "--one", "45", "this"}; + run(); + CHECK(one == "45"); + CHECK(two == "this"); + CHECK(app.remaining().size() == 2U); + + app.allow_extras(CLI::ExtrasMode::AssumeMultipleArguments); + CHECK(app.get_allow_extras_mode() == CLI::ExtrasMode::AssumeMultipleArguments); + args = {"--three", "27", "extra", "--one", "45", "this"}; + one.clear(); + two.clear(); + run(); + CHECK(one == "45"); + CHECK(two == "this"); + CHECK(app.remaining().size() == 3U); +} + +TEST_CASE_METHOD(TApp, "AllowExtrasImmediateError", "[app]") { + + int v1{0}; + int v2{0}; + app.add_option("-f", v1)->trigger_on_parse(); + app.add_option("-x", v2); + args = {"-x", "15", "-f", "17", "-g", "19"}; + CHECK_THROWS_AS(run(), CLI::ExtrasError); + CHECK(v1 == 17); + CHECK(app.remaining().size() == 2U); + args = {"-x", "21", "-f", "23", "-g", "25"}; + app.allow_extras(CLI::ExtrasMode::ErrorImmediately); + CHECK_THROWS_AS(run(), CLI::ExtrasError); + CHECK(v1 == 23); // -f still triggers + CHECK(v2 == 15); + CHECK(app.remaining().empty()); + args = {"-x", "27", "-g", "29", "-f", "31"}; + CHECK_THROWS_AS(run(), CLI::ExtrasError); + CHECK(v1 == 23); // -f did not trigger + CHECK(v2 == 15); + CHECK(app.remaining().empty()); +} + TEST_CASE_METHOD(TApp, "PrefixCommand", "[app]") { int v1{0}; int v2{0}; @@ -2481,8 +2550,6 @@ TEST_CASE_METHOD(TApp, "PrefixCommand", "[app]") { // makes sure the error throws on the rValue version of the parse TEST_CASE_METHOD(TApp, "ExtrasErrorRvalueParse", "[app]") { - args = {"-x", "45", "-f", "27"}; - CHECK_THROWS_AS(app.parse(std::vector({"-x", "45", "-f", "27"})), CLI::ExtrasError); } diff --git a/tests/OptionGroupTest.cpp b/tests/OptionGroupTest.cpp index d453d53d3..18c916581 100644 --- a/tests/OptionGroupTest.cpp +++ b/tests/OptionGroupTest.cpp @@ -706,6 +706,14 @@ TEST_CASE_METHOD(ManyGroups, "ExtrasFallDown", "[optiongroup]") { std::vector extras{"--test1", "--flag", "extra"}; CHECK(extras == app.remaining(true)); CHECK(extras == main->remaining()); + app.allow_extras(CLI::ExtrasMode::Ignore); + + CHECK_NOTHROW(run()); + + CHECK(0u == app.remaining_size(false)); + + CHECK(extras == app.remaining(true)); + CHECK(extras == main->remaining()); } // Test the option Inheritance