Skip to content
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 37 additions & 7 deletions include/CLI/App.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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<ConfigExtrasMode>(mode);
return this;
}

/// ignore extras in config files
App *allow_config_extras(ConfigExtrasMode mode) {
allow_config_extras_ = mode;
return this;
}
Expand Down Expand Up @@ -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_; }
Expand Down Expand Up @@ -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<config_extras_mode>(allow_config_extras_);
}

/// Get a pointer to the help flag.
Option *get_help_ptr() { return help_ptr_; }
Expand Down
42 changes: 32 additions & 10 deletions include/CLI/impl/App_inl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -986,7 +986,7 @@ CLI11_NODISCARD CLI11_INLINE std::vector<std::string> 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<detail::Classifier, std::string> &miss : sub->missing_) {
Expand Down Expand Up @@ -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() != "--") {
Expand Down Expand Up @@ -1555,7 +1555,7 @@ CLI11_INLINE void App::_parse_stream(std::istream &input) {

CLI11_INLINE void App::_parse_config(const std::vector<ConfigItem> &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());
}
}
Expand Down Expand Up @@ -1730,7 +1730,8 @@ CLI11_INLINE bool App::_parse_single(std::vector<std::string> &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();
Expand Down Expand Up @@ -2153,6 +2154,16 @@ App::_parse_arg(std::vector<std::string> &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;
}
Expand Down Expand Up @@ -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<std::string>{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) {
Expand Down
73 changes: 70 additions & 3 deletions tests/AppTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2391,6 +2391,12 @@ TEST_CASE_METHOD(TApp, "AllowExtras", "[app]") {
REQUIRE_NOTHROW(run());
CHECK(val);
CHECK(std::vector<std::string>({"-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]") {
Expand All @@ -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<std::string>({"-x", "45", "-f", "27"}) == app.remaining());
Expand All @@ -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};
Expand Down Expand Up @@ -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<std::string>({"-x", "45", "-f", "27"})), CLI::ExtrasError);
}

Expand Down
8 changes: 8 additions & 0 deletions tests/OptionGroupTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,14 @@ TEST_CASE_METHOD(ManyGroups, "ExtrasFallDown", "[optiongroup]") {
std::vector<std::string> 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
Expand Down