Skip to content

Commit 1d99797

Browse files
Infinite recursion bug (#1316)
fix a bug that led to infinite recursion on subcommand option groups with fallthrough enabled. FIxes #1315 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 7800246 commit 1d99797

File tree

4 files changed

+63
-20
lines changed

4 files changed

+63
-20
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,11 +1077,13 @@ option_groups. These are:
10771077
along with the parent for subcommands with fallthrough.
10781078
- `.get_option_no_throw(name)`: Get an option pointer by option name. This
10791079
function will return a `nullptr` instead of throwing if the option is not
1080-
available.
1080+
available. This method will not search parents of option_options or nameless
1081+
subcommands regardless of fallthrough status 🚧, this behavior is slightly
1082+
different from `get_option`.
10811083
- `.get_options(filter)`: Get the list of all defined option pointers (useful
10821084
for processing the app for custom output formats). If used on a subcommand
10831085
will also get options that are in the parent app if the subcommand has
1084-
fallthrough.
1086+
fallthrough (and is not nameless 🚧).
10851087
- `.parse_order()`: Get the list of option pointers in the order they were
10861088
parsed (including duplicates).
10871089
- `.formatter(std::shared_ptr<formatterBase> fmt)`: Set a custom formatter for

include/CLI/App.hpp

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,7 @@ class App {
776776
throw IncorrectConstruction("option group names may not contain newlines or null characters");
777777
}
778778
auto option_group = std::make_shared<T>(std::move(group_description), group_name, this);
779+
option_group->fallthrough(false);
779780
auto *ptr = option_group.get();
780781
// move to App_p for overload resolution on older gcc versions
781782
App_p app_ptr = std::static_pointer_cast<App>(option_group);
@@ -1139,22 +1140,10 @@ class App {
11391140
CLI11_NODISCARD const Option *get_option_no_throw(std::string option_name) const noexcept;
11401141

11411142
/// Get an option by name
1142-
CLI11_NODISCARD const Option *get_option(std::string option_name) const {
1143-
const auto *opt = get_option_no_throw(option_name);
1144-
if(opt == nullptr) {
1145-
throw OptionNotFound(option_name);
1146-
}
1147-
return opt;
1148-
}
1143+
CLI11_NODISCARD const Option *get_option(std::string option_name) const;
11491144

11501145
/// Get an option by name (non-const version)
1151-
CLI11_NODISCARD Option *get_option(std::string option_name) {
1152-
auto *opt = get_option_no_throw(option_name);
1153-
if(opt == nullptr) {
1154-
throw OptionNotFound(option_name);
1155-
}
1156-
return opt;
1157-
}
1146+
CLI11_NODISCARD Option *get_option(std::string option_name);
11581147

11591148
/// Shortcut bracket operator for getting a pointer to an option
11601149
const Option *operator[](const std::string &option_name) const { return get_option(option_name); }

include/CLI/impl/App_inl.hpp

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -848,7 +848,7 @@ CLI11_INLINE std::vector<const Option *> App::get_options(const std::function<bo
848848
options.insert(options.end(), subcopts.begin(), subcopts.end());
849849
}
850850
}
851-
if(fallthrough_ && parent_ != nullptr) {
851+
if(fallthrough_ && parent_ != nullptr && !name_.empty()) {
852852
const auto *fallthrough_parent = _get_fallthrough_parent();
853853
std::vector<const Option *> subcopts = fallthrough_parent->get_options(filter);
854854
for(const auto *opt : subcopts) {
@@ -879,7 +879,7 @@ CLI11_INLINE std::vector<Option *> App::get_options(const std::function<bool(Opt
879879
options.insert(options.end(), subcopts.begin(), subcopts.end());
880880
}
881881
}
882-
if(fallthrough_ && parent_ != nullptr) {
882+
if(fallthrough_ && parent_ != nullptr && !name_.empty()) {
883883
auto *fallthrough_parent = _get_fallthrough_parent();
884884
std::vector<Option *> subcopts = fallthrough_parent->get_options(filter);
885885
for(auto *opt : subcopts) {
@@ -893,6 +893,36 @@ CLI11_INLINE std::vector<Option *> App::get_options(const std::function<bool(Opt
893893
return options;
894894
}
895895

896+
/// Get an option by name
897+
CLI11_NODISCARD CLI11_INLINE const Option *App::get_option(std::string option_name) const {
898+
const auto *opt = get_option_no_throw(option_name);
899+
if(opt == nullptr) {
900+
if(fallthrough_ && parent_ != nullptr && name_.empty()) {
901+
// as a special case option groups with fallthrough enabled can also check the parent for options if the
902+
// option is not found in the group this will not recurse as the internal call is to the no_throw version
903+
// which will not check the parent again for option groups even with fallthrough enabled
904+
return _get_fallthrough_parent()->get_option(option_name);
905+
}
906+
throw OptionNotFound(option_name);
907+
}
908+
return opt;
909+
}
910+
911+
/// Get an option by name (non-const version)
912+
CLI11_NODISCARD CLI11_INLINE Option *App::get_option(std::string option_name) {
913+
auto *opt = get_option_no_throw(option_name);
914+
if(opt == nullptr) {
915+
if(fallthrough_ && parent_ != nullptr && name_.empty()) {
916+
// as a special case option groups with fallthrough enabled can also check the parent for options if the
917+
// option is not found in the group this will not recurse as the internal call is to the no_throw version
918+
// which will not check the parent again for option groups even with fallthrough enabled
919+
return _get_fallthrough_parent()->get_option(option_name);
920+
}
921+
throw OptionNotFound(option_name);
922+
}
923+
return opt;
924+
}
925+
896926
CLI11_NODISCARD CLI11_INLINE Option *App::get_option_no_throw(std::string option_name) noexcept {
897927
for(Option_p &opt : options_) {
898928
if(opt->check_name(option_name)) {
@@ -908,7 +938,9 @@ CLI11_NODISCARD CLI11_INLINE Option *App::get_option_no_throw(std::string option
908938
}
909939
}
910940
}
911-
if(fallthrough_ && parent_ != nullptr) {
941+
if(fallthrough_ && parent_ != nullptr && !name_.empty()) {
942+
// if there is fallthrough and a parent and this is not an option_group then also check the parent for the
943+
// option
912944
return _get_fallthrough_parent()->get_option_no_throw(option_name);
913945
}
914946
return nullptr;
@@ -929,7 +961,7 @@ CLI11_NODISCARD CLI11_INLINE const Option *App::get_option_no_throw(std::string
929961
}
930962
}
931963
}
932-
if(fallthrough_ && parent_ != nullptr) {
964+
if(fallthrough_ && parent_ != nullptr && !name_.empty()) {
933965
return _get_fallthrough_parent()->get_option_no_throw(option_name);
934966
}
935967
return nullptr;

tests/OptionGroupTest.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,27 @@ TEST_CASE_METHOD(ManyGroups, "OptionFind", "[optiongroup]") {
843843
g1->fallthrough();
844844
auto *opt_name = g1->get_option("--base");
845845
CHECK(opt_name == opt_main);
846+
CHECK_THROWS_AS(g1->get_option("--notfound"), CLI::OptionNotFound);
846847
auto const *g1_const = g1;
847848
const auto *opt_name_const = g1_const->get_option("--base");
848849
CHECK(opt_name_const == opt_main);
850+
CHECK_THROWS_AS(g1_const->get_option("--notfound"), CLI::OptionNotFound);
851+
}
852+
853+
// from https://github.com/CLIUtils/CLI11/issues/1315
854+
TEST_CASE_METHOD(TApp, "SubcommandOptionGroupWithFallthrough", "[optiongroup]") {
855+
// code from https://github.com/The0Dev
856+
bool flag{false};
857+
std::string str;
858+
app.add_flag("--flag,!--no-flag", flag, "Enable a flag");
859+
860+
// Disabling this prevents the issue:
861+
app.fallthrough(true);
862+
863+
auto *sub = app.add_subcommand("sub", "Execute a subcommand");
864+
865+
auto *group = sub->add_option_group("GROUP");
866+
867+
// possible failure
868+
CHECK_NOTHROW(group->add_option("-p,--path", str, "An option"));
849869
}

0 commit comments

Comments
 (0)