diff --git a/CMakeLists.txt b/CMakeLists.txt index bad2b00..3473361 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,9 +2,9 @@ cmake_minimum_required(VERSION 3.4) # Note: this is a header only library. If you have an older CMake than 3.4, # just add the CLI11/include directory and that's all you need to do. -# Make sure users don't get warnings on a tested (3.4 to 3.22) version +# Make sure users don't get warnings on a tested (3.4 to 3.24) version # of CMake. For most of the policies, the new version is better (hence the change). -# We don't use the 3.4...3.21 syntax because of a bug in an older MSVC's +# We don't use the 3.4...3.24 syntax because of a bug in an older MSVC's # built-in and modified CMake 3.11 if(${CMAKE_VERSION} VERSION_LESS 3.24) cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) diff --git a/README.md b/README.md index eee85a1..fdf0b0b 100644 --- a/README.md +++ b/README.md @@ -938,6 +938,20 @@ nameless subcommands are allowed. Callbacks for nameless subcommands are only triggered if any options from the subcommand were parsed. Subcommand names given through the `add_subcommand` method have the same restrictions as option names. +🚧 Options or flags in a subcommand may be directly specified using dot notation + +- `--subcommand.long=val` (long subcommand option) +- `--subcommand.long val` (long subcommand option) +- `--subcommand.f=val` (short form subcommand option) +- `--subcommand.f val` (short form subcommand option) +- `--subcommand.f` (short form subcommand flag) +- `--subcommand1.subsub.f val` (short form nested subcommand option) + +The use of dot notation in this form is equivalent `--subcommand.long ` => +`subcommand --long ++`. Nested subcommands also work `"sub1.subsub"` +would trigger the subsub subcommand in `sub1`. This is equivalent to "sub1 +subsub" + #### Subcommand options There are several options that are supported on the main app and subcommands and diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 512cf9b..27aa8da 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -1285,8 +1285,9 @@ class App { bool _parse_subcommand(std::vector &args); /// Parse a short (false) or long (true) argument, must be at the top of the list + /// if local_processing_only is set to true then fallthrough is disabled will return false if not found /// return true if the argument was processed or false if nothing was done - bool _parse_arg(std::vector &args, detail::Classifier current_type); + bool _parse_arg(std::vector &args, detail::Classifier current_type, bool local_processing_only); /// Trigger the pre_parse callback if needed void _trigger_pre_parse(std::size_t remaining_args); diff --git a/include/CLI/impl/App_inl.hpp b/include/CLI/impl/App_inl.hpp index 1a8d250..b7ef7cf 100644 --- a/include/CLI/impl/App_inl.hpp +++ b/include/CLI/impl/App_inl.hpp @@ -976,6 +976,16 @@ CLI11_NODISCARD CLI11_INLINE detail::Classifier App::_recognize(const std::strin return detail::Classifier::WINDOWS_STYLE; if((current == "++") && !name_.empty() && parent_ != nullptr) return detail::Classifier::SUBCOMMAND_TERMINATOR; + auto dotloc = current.find_first_of('.'); + if(dotloc != std::string::npos) { + auto *cm = _find_subcommand(current.substr(0, dotloc), true, ignore_used_subcommands); + if(cm != nullptr) { + auto res = cm->_recognize(current.substr(dotloc + 1), ignore_used_subcommands); + if(res == detail::Classifier::SUBCOMMAND) { + return res; + } + } + } return detail::Classifier::NONE; } @@ -1458,7 +1468,7 @@ CLI11_INLINE bool App::_parse_single(std::vector &args, bool &posit case detail::Classifier::SHORT: case detail::Classifier::WINDOWS_STYLE: // If already parsed a subcommand, don't accept options_ - _parse_arg(args, classifier); + _parse_arg(args, classifier, false); break; case detail::Classifier::NONE: // Probably a positional or something for a parent (sub)command @@ -1646,6 +1656,17 @@ CLI11_INLINE bool App::_parse_subcommand(std::vector &args) { return true; } auto *com = _find_subcommand(args.back(), true, true); + if(com == nullptr) { + // the main way to get here is using .notation + auto dotloc = args.back().find_first_of('.'); + if(dotloc != std::string::npos) { + com = _find_subcommand(args.back().substr(0, dotloc), true, true); + if(com != nullptr) { + args.back() = args.back().substr(dotloc + 1); + args.push_back(com->get_display_name()); + } + } + } if(com != nullptr) { args.pop_back(); if(!com->silent_) { @@ -1668,7 +1689,8 @@ CLI11_INLINE bool App::_parse_subcommand(std::vector &args) { return false; } -CLI11_INLINE bool App::_parse_arg(std::vector &args, detail::Classifier current_type) { +CLI11_INLINE bool +App::_parse_arg(std::vector &args, detail::Classifier current_type, bool local_processing_only) { std::string current = args.back(); @@ -1710,7 +1732,7 @@ CLI11_INLINE bool App::_parse_arg(std::vector &args, detail::Classi if(op_ptr == std::end(options_)) { for(auto &subc : subcommands_) { if(subc->name_.empty() && !subc->disabled_) { - if(subc->_parse_arg(args, current_type)) { + if(subc->_parse_arg(args, current_type, local_processing_only)) { if(!subc->pre_parse_called_) { subc->_trigger_pre_parse(args.size()); } @@ -1724,9 +1746,57 @@ CLI11_INLINE bool App::_parse_arg(std::vector &args, detail::Classi return false; } + // now check for '.' notation of subcommands + auto dotloc = arg_name.find_first_of('.', 1); + if(dotloc != std::string::npos) { + // using dot notation is equivalent to single argument subcommand + auto *sub = _find_subcommand(arg_name.substr(0, dotloc), true, false); + if(sub != nullptr) { + auto v = args.back(); + args.pop_back(); + arg_name = arg_name.substr(dotloc + 1); + if(arg_name.size() > 1) { + args.push_back(std::string("--") + v.substr(dotloc + 3)); + current_type = detail::Classifier::LONG; + } else { + auto nval = v.substr(dotloc + 2); + nval.front() = '-'; + if(nval.size() > 2) { + // '=' not allowed in short form arguments + args.push_back(nval.substr(3)); + nval.resize(2); + } + args.push_back(nval); + current_type = detail::Classifier::SHORT; + } + auto val = sub->_parse_arg(args, current_type, true); + if(val) { + if(!sub->silent_) { + parsed_subcommands_.push_back(sub); + } + // deal with preparsing + increment_parsed(); + _trigger_pre_parse(args.size()); + // run the parse complete callback since the subcommand processing is now complete + if(sub->parse_complete_callback_) { + sub->_process_env(); + sub->_process_callbacks(); + sub->_process_help_flags(); + sub->_process_requirements(); + sub->run_callback(false, true); + } + return true; + } + args.pop_back(); + args.push_back(v); + } + } + if(local_processing_only) { + return false; + } // If a subcommand, try the main command if(parent_ != nullptr && fallthrough_) - return _get_fallthrough_parent()->_parse_arg(args, current_type); + return _get_fallthrough_parent()->_parse_arg(args, current_type, false); // Otherwise, add to missing args.pop_back(); diff --git a/include/CLI/impl/Split_inl.hpp b/include/CLI/impl/Split_inl.hpp index bb05a86..d974f80 100644 --- a/include/CLI/impl/Split_inl.hpp +++ b/include/CLI/impl/Split_inl.hpp @@ -34,7 +34,7 @@ CLI11_INLINE bool split_short(const std::string ¤t, std::string &name, std } CLI11_INLINE bool split_long(const std::string ¤t, std::string &name, std::string &value) { - if(current.size() > 2 && current.substr(0, 2) == "--" && valid_first_char(current[2])) { + if(current.size() > 2 && current.compare(0, 2, "--") == 0 && valid_first_char(current[2])) { auto loc = current.find_first_of('='); if(loc != std::string::npos) { name = current.substr(2, loc - 2); diff --git a/tests/AppTest.cpp b/tests/AppTest.cpp index de90f64..fc5dee0 100644 --- a/tests/AppTest.cpp +++ b/tests/AppTest.cpp @@ -2118,21 +2118,23 @@ TEST_CASE_METHOD(TApp, "AllowExtrasArgModify", "[app]") { TEST_CASE_METHOD(TApp, "CheckShortFail", "[app]") { args = {"--two"}; - CHECK_THROWS_AS(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::SHORT), CLI::HorribleError); + CHECK_THROWS_AS(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::SHORT, false), + CLI::HorribleError); } // Test horrible error TEST_CASE_METHOD(TApp, "CheckLongFail", "[app]") { args = {"-t"}; - CHECK_THROWS_AS(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::LONG), CLI::HorribleError); + CHECK_THROWS_AS(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::LONG, false), + CLI::HorribleError); } // Test horrible error TEST_CASE_METHOD(TApp, "CheckWindowsFail", "[app]") { args = {"-t"}; - CHECK_THROWS_AS(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::WINDOWS_STYLE), + CHECK_THROWS_AS(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::WINDOWS_STYLE, false), CLI::HorribleError); } @@ -2140,7 +2142,8 @@ TEST_CASE_METHOD(TApp, "CheckWindowsFail", "[app]") { TEST_CASE_METHOD(TApp, "CheckOtherFail", "[app]") { args = {"-t"}; - CHECK_THROWS_AS(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::NONE), CLI::HorribleError); + CHECK_THROWS_AS(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::NONE, false), + CLI::HorribleError); } // Test horrible error diff --git a/tests/SubcommandTest.cpp b/tests/SubcommandTest.cpp index f087598..01512d5 100644 --- a/tests/SubcommandTest.cpp +++ b/tests/SubcommandTest.cpp @@ -1410,6 +1410,15 @@ TEST_CASE_METHOD(ManySubcommands, "SubcommandNeedsOptionsCallbackOrdering", "[su CHECK_NOTHROW(run()); } +TEST_CASE_METHOD(ManySubcommands, "SubcommandParseCompleteDotNotation", "[subcom]") { + int count{0}; + sub1->add_flag("--flag1"); + sub1->parse_complete_callback([&count]() { ++count; }); + args = {"--sub1.flag1", "--sub1.flag1"}; + run(); + CHECK(count == 2); +} + TEST_CASE_METHOD(ManySubcommands, "SubcommandNeedsFail", "[subcom]") { auto *opt = app.add_flag("--subactive"); @@ -1984,3 +1993,126 @@ TEST_CASE_METHOD(TApp, "SubcommandInOptionGroupCallbackCount", "[subcom]") { run(); CHECK(subcount == 1); } + +TEST_CASE_METHOD(TApp, "DotNotationSubcommand", "[subcom]") { + std::string v1, v2, vbase; + + auto *sub1 = app.add_subcommand("sub1"); + auto *sub2 = app.add_subcommand("sub2"); + sub1->add_option("--value", v1); + sub2->add_option("--value", v2); + app.add_option("--value", vbase); + args = {"--sub1.value", "val1"}; + run(); + CHECK(v1 == "val1"); + + args = {"--sub2.value", "val2", "--value", "base"}; + run(); + CHECK(v2 == "val2"); + CHECK(vbase == "base"); + v1.clear(); + v2.clear(); + vbase.clear(); + + args = {"--sub2.value=val2", "--value=base"}; + run(); + CHECK(v2 == "val2"); + CHECK(vbase == "base"); + + auto subs = app.get_subcommands(); + REQUIRE(!subs.empty()); + CHECK(subs.front()->get_name() == "sub2"); +} + +TEST_CASE_METHOD(TApp, "DotNotationSubcommandSingleChar", "[subcom]") { + std::string v1, v2, vbase; + + auto *sub1 = app.add_subcommand("sub1"); + auto *sub2 = app.add_subcommand("sub2"); + sub1->add_option("-v", v1); + sub2->add_option("-v", v2); + app.add_option("-v", vbase); + args = {"--sub1.v", "val1"}; + run(); + CHECK(v1 == "val1"); + + args = {"--sub2.v", "val2", "-v", "base"}; + run(); + CHECK(v2 == "val2"); + CHECK(vbase == "base"); + v1.clear(); + v2.clear(); + vbase.clear(); + + args = {"--sub2.v=val2", "-vbase"}; + run(); + CHECK(v2 == "val2"); + CHECK(vbase == "base"); + + auto subs = app.get_subcommands(); + REQUIRE(!subs.empty()); + CHECK(subs.front()->get_name() == "sub2"); +} + +TEST_CASE_METHOD(TApp, "DotNotationSubcommandRecusive", "[subcom]") { + std::string v1, v2, v3, vbase; + + auto *sub1 = app.add_subcommand("sub1"); + auto *sub2 = sub1->add_subcommand("sub2"); + auto *sub3 = sub2->add_subcommand("sub3"); + + sub1->add_option("--value", v1); + sub2->add_option("--value", v2); + sub3->add_option("--value", v3); + app.add_option("--value", vbase); + args = {"--sub1.sub2.sub3.value", "val1"}; + run(); + CHECK(v3 == "val1"); + + args = {"--sub1.sub2.value", "val2"}; + run(); + CHECK(v2 == "val2"); + + args = {"--sub1.sub2.bob", "val2"}; + CHECK_THROWS_AS(run(), CLI::ExtrasError); + app.allow_extras(); + CHECK_NOTHROW(run()); + auto extras = app.remaining(); + CHECK(extras.size() == 2); + CHECK(extras.front() == "--sub1.sub2.bob"); +} + +TEST_CASE_METHOD(TApp, "DotNotationSubcommandRecusive2", "[subcom]") { + std::string v1, v2, v3, vbase; + + auto *sub1 = app.add_subcommand("sub1"); + auto *sub2 = sub1->add_subcommand("sub2"); + auto *sub3 = sub2->add_subcommand("sub3"); + + sub1->add_option("--value", v1); + sub2->add_option("--value", v2); + sub3->add_option("--value", v3); + app.add_option("--value", vbase); + args = {"sub1.sub2.sub3", "--value", "val1"}; + run(); + CHECK(v3 == "val1"); + + args = {"sub1.sub2", "--value", "val2"}; + run(); + CHECK(v2 == "val2"); + + args = {"sub1.bob", "--value", "val2"}; + CHECK_THROWS_AS(run(), CLI::ExtrasError); + + args = {"sub1.sub2.bob", "--value", "val2"}; + CHECK_THROWS_AS(run(), CLI::ExtrasError); + + args = {"sub1.sub2.sub3.bob", "--value", "val2"}; + CHECK_THROWS_AS(run(), CLI::ExtrasError); + + app.allow_extras(); + CHECK_NOTHROW(run()); + auto extras = app.remaining(); + CHECK(extras.size() == 1); + CHECK(extras.front() == "sub1.sub2.sub3.bob"); +}