Extras configuration (#1270)

Allow more control over how extras are interpreted. Specifically
allowing some assumptions to be made about arguments to unrecognized
options. With `CLI::ExtrasMode::AssumeMultipleArguments` all positional
arguments following an unrecognized option will be considered extra as
well even if there are open positional arguments. With
`CLI::ExtrasMode::AssumeSingleArgument` a positional argument following
an unrecognized option will also be considered unrecognized and go into
the `remaining()` bin.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Philip Top
2026-01-02 10:05:46 -08:00
committed by GitHub
parent 457839d482
commit 20e9132dfc
5 changed files with 165 additions and 20 deletions

View File

@@ -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<ConfigExtrasMode>(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<config_extras_mode>(allow_config_extras_);
}
/// Get a pointer to the help flag.
Option *get_help_ptr() { return help_ptr_; }

View File

@@ -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_) {
@@ -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<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());
}
}
@@ -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();
@@ -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;
}
@@ -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) {