prefix_command tests and improvements (#1266)

adding a PrefixCommandMode option to the prefix_command to allow
specification of a separator and catch other errors

Addresses #1264

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Philip Top
2025-12-17 06:26:16 -08:00
committed by GitHub
parent 6919ea8624
commit fe8f9f7db3
5 changed files with 106 additions and 35 deletions

View File

@@ -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

View File

@@ -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<int> vals;
app.add_option("--vals,-v", vals)->expected(-1);

View File

@@ -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<App>;
@@ -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<bool>(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<std::string> &args);
/// Internal function to recursively increment the parsed counter on the current app as well unnamed subcommands
void increment_parsed();

View File

@@ -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<std::string> &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<std::string> &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<std::string> &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<std::string> &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<std::string> &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);

View File

@@ -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]") {