diff --git a/README.md b/README.md index d2fd20ee..17849c13 100644 --- a/README.md +++ b/README.md @@ -1128,7 +1128,14 @@ option_groups. These are: - `.prefix_command()`: Like `allow_extras`, but stop processing immediately on the first unrecognized item. All subsequent arguments are placed in the remaining_arg list. It is ideal for allowing your app or subcommand to be a - "prefix" to calling another app. + "prefix" to calling another app. Can be called with a `bool` value to turn on + or off +- `.prefix_command(CLI::PrefixCommandMode)`: specify the prefix_command mode to + use. `PrefixCommandMode::on` and `PrefixCommandMode::off` are the same as + `prefix_command(true)` or `prefix_command(false)`. Calling with + `PrefixCommandMode::separator_only` will only trigger prefix command mode by + the use of the subcommand separator `--` other unrecognized arguments would be + considered an error depending on whether `allow_extras` was set or not. - `.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/examples/prefix_command.cpp b/examples/prefix_command.cpp index eee038ed..f3bdaa07 100644 --- a/examples/prefix_command.cpp +++ b/examples/prefix_command.cpp @@ -12,7 +12,7 @@ int main(int argc, char **argv) { CLI::App app("Prefix command app"); - app.prefix_command(); + app.prefix_command(CLI::PrefixCommandMode::On); std::vector vals; app.add_option("--vals,-v", vals)->expected(-1); diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index b369a3f8..969b061f 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -67,9 +67,13 @@ CLI11_INLINE std::string help(const App *app, const Error &e); } // namespace FailureMessage /// 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 +/// 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 }; + class App; using App_p = std::shared_ptr; @@ -119,7 +123,7 @@ class App { config_extras_mode allow_config_extras_{config_extras_mode::ignore}; /// If true, cease processing on an unrecognized option (implies allow_extras) INHERITABLE - bool prefix_command_{false}; + PrefixCommandMode prefix_command_{PrefixCommandMode::Off}; /// If set to true the name was automatically generated from the command line vs a user set name bool has_automatic_name_{false}; @@ -474,7 +478,14 @@ class App { /// Do not parse anything after the first unrecognized option (if true) all remaining arguments are stored in /// remaining args App *prefix_command(bool is_prefix = true) { - prefix_command_ = is_prefix; + prefix_command_ = is_prefix ? PrefixCommandMode::On : PrefixCommandMode::Off; + return this; + } + + /// Do not parse anything after the first unrecognized option (if true) all remaining arguments are stored in + /// remaining args + App *prefix_command(PrefixCommandMode mode) { + prefix_command_ = mode; return this; } @@ -1162,7 +1173,10 @@ class App { CLI11_NODISCARD std::size_t get_require_option_max() const { return require_option_max_; } /// Get the prefix command status - CLI11_NODISCARD bool get_prefix_command() const { return prefix_command_; } + CLI11_NODISCARD bool get_prefix_command() const { return static_cast(prefix_command_); } + + /// Get the prefix command status + 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_; } @@ -1319,10 +1333,6 @@ class App { /// Throw an error if anything is left over and should not be. void _process_extras(); - /// Throw an error if anything is left over and should not be. - /// Modifies the args to fill in the missing items before throwing. - void _process_extras(std::vector &args); - /// Internal function to recursively increment the parsed counter on the current app as well unnamed subcommands void increment_parsed(); diff --git a/include/CLI/impl/App_inl.hpp b/include/CLI/impl/App_inl.hpp index db9fb5d9..b894c8c4 100644 --- a/include/CLI/impl/App_inl.hpp +++ b/include/CLI/impl/App_inl.hpp @@ -1072,7 +1072,7 @@ CLI11_INLINE void App::_configure() { } if(app->name_.empty()) { app->fallthrough_ = false; // make sure fallthrough_ is false to prevent infinite loop - app->prefix_command_ = false; + app->prefix_command_ = PrefixCommandMode::Off; } // make sure the parent is set to be this object in preparation for parse app->parent_ = this; @@ -1461,34 +1461,26 @@ CLI11_INLINE void App::_process() { } CLI11_INLINE void App::_process_extras() { - if(!(allow_extras_ || prefix_command_)) { + if(!allow_extras_ && 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) { + std::size_t num_left_over = remaining_size(); + if(num_left_over > 0) { + if(remaining(false).front() != "--") { + throw ExtrasError(name_, remaining(false)); + } + } + } for(App_p &sub : subcommands_) { if(sub->count() > 0) sub->_process_extras(); } } -CLI11_INLINE void App::_process_extras(std::vector &args) { - if(!(allow_extras_ || prefix_command_)) { - std::size_t num_left_over = remaining_size(); - if(num_left_over > 0) { - args = remaining(false); - throw ExtrasError(name_, args); - } - } - - for(App_p &sub : subcommands_) { - if(sub->count() > 0) - sub->_process_extras(args); - } -} - CLI11_INLINE void App::increment_parsed() { ++parsed_; for(App_p &sub : subcommands_) { @@ -1512,8 +1504,7 @@ CLI11_INLINE void App::_parse(std::vector &args) { _process(); // Throw error if any items are left over (depending on settings) - _process_extras(args); - + _process_extras(); // Convert missing (pairs) to extras (string only) ready for processing in another app args = remaining_for_passthrough(false); } else if(parse_complete_callback_) { @@ -1738,7 +1729,13 @@ CLI11_INLINE bool App::_parse_single(std::vector &args, bool &posit case detail::Classifier::POSITIONAL_MARK: args.pop_back(); positional_only = true; - if((!_has_remaining_positionals()) && (parent_ != nullptr)) { + if(get_prefix_command()) { + _move_to_missing(classifier, "--"); + while(!args.empty()) { + missing_.emplace_back(detail::Classifier::NONE, args.back()); + args.pop_back(); + } + } else if((!_has_remaining_positionals()) && (parent_ != nullptr)) { retval = false; } else { _move_to_missing(classifier, "--"); @@ -1922,9 +1919,9 @@ CLI11_INLINE bool App::_parse_positional(std::vector &args, bool ha /// We are out of other options this goes to missing _move_to_missing(detail::Classifier::NONE, positional); args.pop_back(); - if(prefix_command_) { + if(get_prefix_command()) { while(!args.empty()) { - _move_to_missing(detail::Classifier::NONE, args.back()); + missing_.emplace_back(detail::Classifier::NONE, args.back()); args.pop_back(); } } @@ -2151,6 +2148,12 @@ App::_parse_arg(std::vector &args, detail::Classifier current_type, // Otherwise, add to missing args.pop_back(); _move_to_missing(current_type, current); + if(get_prefix_command_mode() == PrefixCommandMode::On) { + while(!args.empty()) { + missing_.emplace_back(detail::Classifier::NONE, args.back()); + args.pop_back(); + } + } return true; } @@ -2340,11 +2343,11 @@ CLI11_NODISCARD CLI11_INLINE const std::string &App::_compare_subcommand_names(c } CLI11_INLINE void App::_move_to_missing(detail::Classifier val_type, const std::string &val) { - if(allow_extras_ || subcommands_.empty()) { + if(allow_extras_ || subcommands_.empty() || get_prefix_command()) { missing_.emplace_back(val_type, val); return; } - // allow extra arguments to be places in an option group if it is allowed there + // 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_) { subc->missing_.emplace_back(val_type, val); diff --git a/tests/AppTest.cpp b/tests/AppTest.cpp index 922f5a3c..8aa40cfd 100644 --- a/tests/AppTest.cpp +++ b/tests/AppTest.cpp @@ -2427,6 +2427,57 @@ TEST_CASE_METHOD(TApp, "AllowExtrasCascade", "[app]") { CHECK(45 == v1); CHECK(27 == v2); } + +TEST_CASE_METHOD(TApp, "PrefixCommand", "[app]") { + int v1{0}; + int v2{0}; + app.add_option("-f", v1); + app.add_option("-x", v2); + app.prefix_command(); + args = {"-x", "45", "-f", "27"}; + run(); + auto rem = app.remaining(); + CHECK(rem.empty()); + + args = {"-x", "45", "-f", "27", "--test", "23"}; + + run(); + rem = app.remaining(); + CHECK(rem.size() == 2U); + + args = {"-x", "45", "-f", "27", "--", "--test", "23"}; + + run(); + rem = app.remaining(); + CHECK(rem.size() == 3U); + + args = {"-x", "45", "--test4", "-f", "27", "--test", "23"}; + + run(); + rem = app.remaining(); + CHECK(rem.size() == 5U); + + app.prefix_command(CLI::PrefixCommandMode::SeparatorOnly); + CHECK_THROWS_AS(run(), CLI::ExtrasError); + + args = {"-x", "45", "positional", "-f", "27", "--test", "23"}; + CHECK_THROWS_AS(run(), CLI::ExtrasError); + + args = {"-x", "45", "-f", "27", "--", "--test", "23"}; + + run(); + rem = app.remaining(); + CHECK(rem.size() == 3U); + + args = {"-x", "45", "--test4", "-f", "27", "--", "--test", "23"}; + + CHECK_THROWS_AS(run(), CLI::ExtrasError); + app.allow_extras(true); + run(); + rem = app.remaining(); + CHECK(rem.size() == 4U); +} + // makes sure the error throws on the rValue version of the parse TEST_CASE_METHOD(TApp, "ExtrasErrorRvalueParse", "[app]") {