diff --git a/README.md b/README.md index 6ad2d1168..dda71fbd5 100644 --- a/README.md +++ b/README.md @@ -1161,7 +1161,10 @@ option_groups. These are: `prefix_command(true)` and `prefix_command(false)`. Calling with `PrefixCommandMode::SeparatorOnly` will only trigger prefix command mode with the subcommand separator `--`; other unrecognized arguments are considered an - error unless `allow_extras` is enabled. + error unless `allow_extras` is enabled. Calling with + `PrefixCommandMode::PositionalOnly` will only trigger prefix command mode at + the first positional argument or the separator `--`; unrecognized options do + not stop processing and are collected in the remaining_arg list. - `.usage(message)`: Replace text to appear at the start of the help string after description. - `.usage(std::string())`: Set a callback to generate a string that will appear diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 1274aebea..1b610c3bc 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -84,8 +84,9 @@ enum class config_extras_mode : std::uint8_t { error = 0, ignore, ignore_all, ca /// @brief enumeration of prefix command modes, separator requires that the first extra argument be a "--", other /// unrecognized arguments will cause an error. on allows the first extra to trigger prefix mode regardless of other -/// recognized options -enum class PrefixCommandMode : std::uint8_t { Off = 0, SeparatorOnly = 1, On = 2 }; +/// recognized options. positional_only triggers prefix mode only at the first positional argument (or "--"); +/// unrecognized options do not stop parsing and are collected as extras +enum class PrefixCommandMode : std::uint8_t { Off = 0, SeparatorOnly = 1, On = 2, PositionalOnly = 3 }; class App; @@ -501,7 +502,8 @@ class App { /// Enable or disable prefix command mode. If enabled, parsing stops at the /// first unrecognized option and all remaining arguments are stored in - /// remaining args. + /// remaining args. Use PrefixCommandMode::PositionalOnly to only stop at + /// positional arguments (or the `--` separator). App *prefix_command(bool is_prefix = true) { prefix_command_ = is_prefix ? PrefixCommandMode::On : PrefixCommandMode::Off; return this; diff --git a/include/CLI/impl/App_inl.hpp b/include/CLI/impl/App_inl.hpp index 42292ca4b..78882ac98 100644 --- a/include/CLI/impl/App_inl.hpp +++ b/include/CLI/impl/App_inl.hpp @@ -2187,7 +2187,8 @@ App::_parse_arg(std::vector &args, detail::Classifier current_type, if(parent_ != nullptr && fallthrough_) return _get_fallthrough_parent()->_parse_arg(args, current_type, false); - // Otherwise, add to missing + // Otherwise, add to missing. In PositionalOnly mode an unrecognized option is left as an + // extra and parsing continues so later registered options are still matched (#1374). args.pop_back(); _move_to_missing(current_type, current); if(get_prefix_command_mode() == PrefixCommandMode::On) { diff --git a/tests/AppTest.cpp b/tests/AppTest.cpp index 982a6a300..886a92e15 100644 --- a/tests/AppTest.cpp +++ b/tests/AppTest.cpp @@ -2613,6 +2613,39 @@ TEST_CASE_METHOD(TApp, "PrefixCommand", "[app]") { CHECK(rem.size() == 4U); } +// PrefixCommandMode::PositionalOnly must not stop parsing at an unrecognized *option*; a +// registered option after one still gets recognized regardless of order (#1374) +TEST_CASE_METHOD(TApp, "PrefixCommandPositionalOnly", "[app]") { + bool known{false}; + app.add_flag("--known", known); + app.prefix_command(CLI::PrefixCommandMode::PositionalOnly); + + args = {"--known", "--unknown"}; + run(); + CHECK(known); + CHECK(app.remaining().size() == 1U); + + known = false; + args = {"--unknown", "--known"}; + run(); + CHECK(known); + CHECK(app.remaining().size() == 1U); // only "--unknown" left over + + // a positional still stops parsing and dumps everything after it + known = false; + args = {"positional", "--known"}; + run(); + CHECK_FALSE(known); + CHECK(app.remaining() == std::vector{"positional", "--known"}); + + // the "--" separator also stops parsing + known = false; + args = {"--", "--known"}; + run(); + CHECK_FALSE(known); + CHECK(app.remaining() == std::vector{"--", "--known"}); +} + // makes sure the error throws on the rValue version of the parse TEST_CASE_METHOD(TApp, "ExtrasErrorRvalueParse", "[app]") {