Commit 4c7c8ddc45d2ef74584e5cd945f7a4d27c987748

Authored by Philip Top
Committed by GitHub
1 parent d3505540

feat: dot notation (#789)

* Allow using dot notation for subcommand arguments  such as --sub1.field

* add tests for dot notation for subcommands

* style: pre-commit.ci fixes

* add test for short form arguments in dot notation

* style: pre-commit.ci fixes

* add _pre_parse_callback_ support using dot notation

* style: pre-commit.ci fixes

* update cmake tests to include 3.24

* change line endings

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
CMakeLists.txt
@@ -2,9 +2,9 @@ cmake_minimum_required(VERSION 3.4) @@ -2,9 +2,9 @@ cmake_minimum_required(VERSION 3.4)
2 # Note: this is a header only library. If you have an older CMake than 3.4, 2 # Note: this is a header only library. If you have an older CMake than 3.4,
3 # just add the CLI11/include directory and that's all you need to do. 3 # just add the CLI11/include directory and that's all you need to do.
4 4
5 -# Make sure users don't get warnings on a tested (3.4 to 3.22) version 5 +# Make sure users don't get warnings on a tested (3.4 to 3.24) version
6 # of CMake. For most of the policies, the new version is better (hence the change). 6 # of CMake. For most of the policies, the new version is better (hence the change).
7 -# We don't use the 3.4...3.21 syntax because of a bug in an older MSVC's 7 +# We don't use the 3.4...3.24 syntax because of a bug in an older MSVC's
8 # built-in and modified CMake 3.11 8 # built-in and modified CMake 3.11
9 if(${CMAKE_VERSION} VERSION_LESS 3.24) 9 if(${CMAKE_VERSION} VERSION_LESS 3.24)
10 cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) 10 cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
README.md
@@ -938,6 +938,20 @@ nameless subcommands are allowed. Callbacks for nameless subcommands are only @@ -938,6 +938,20 @@ nameless subcommands are allowed. Callbacks for nameless subcommands are only
938 triggered if any options from the subcommand were parsed. Subcommand names given 938 triggered if any options from the subcommand were parsed. Subcommand names given
939 through the `add_subcommand` method have the same restrictions as option names. 939 through the `add_subcommand` method have the same restrictions as option names.
940 940
  941 +🚧 Options or flags in a subcommand may be directly specified using dot notation
  942 +
  943 +- `--subcommand.long=val` (long subcommand option)
  944 +- `--subcommand.long val` (long subcommand option)
  945 +- `--subcommand.f=val` (short form subcommand option)
  946 +- `--subcommand.f val` (short form subcommand option)
  947 +- `--subcommand.f` (short form subcommand flag)
  948 +- `--subcommand1.subsub.f val` (short form nested subcommand option)
  949 +
  950 +The use of dot notation in this form is equivalent `--subcommand.long <args>` =>
  951 +`subcommand --long <args> ++`. Nested subcommands also work `"sub1.subsub"`
  952 +would trigger the subsub subcommand in `sub1`. This is equivalent to "sub1
  953 +subsub"
  954 +
941 #### Subcommand options 955 #### Subcommand options
942 956
943 There are several options that are supported on the main app and subcommands and 957 There are several options that are supported on the main app and subcommands and
include/CLI/App.hpp
@@ -1285,8 +1285,9 @@ class App { @@ -1285,8 +1285,9 @@ class App {
1285 bool _parse_subcommand(std::vector<std::string> &args); 1285 bool _parse_subcommand(std::vector<std::string> &args);
1286 1286
1287 /// Parse a short (false) or long (true) argument, must be at the top of the list 1287 /// Parse a short (false) or long (true) argument, must be at the top of the list
  1288 + /// if local_processing_only is set to true then fallthrough is disabled will return false if not found
1288 /// return true if the argument was processed or false if nothing was done 1289 /// return true if the argument was processed or false if nothing was done
1289 - bool _parse_arg(std::vector<std::string> &args, detail::Classifier current_type); 1290 + bool _parse_arg(std::vector<std::string> &args, detail::Classifier current_type, bool local_processing_only);
1290 1291
1291 /// Trigger the pre_parse callback if needed 1292 /// Trigger the pre_parse callback if needed
1292 void _trigger_pre_parse(std::size_t remaining_args); 1293 void _trigger_pre_parse(std::size_t remaining_args);
include/CLI/impl/App_inl.hpp
@@ -976,6 +976,16 @@ CLI11_NODISCARD CLI11_INLINE detail::Classifier App::_recognize(const std::strin @@ -976,6 +976,16 @@ CLI11_NODISCARD CLI11_INLINE detail::Classifier App::_recognize(const std::strin
976 return detail::Classifier::WINDOWS_STYLE; 976 return detail::Classifier::WINDOWS_STYLE;
977 if((current == "++") && !name_.empty() && parent_ != nullptr) 977 if((current == "++") && !name_.empty() && parent_ != nullptr)
978 return detail::Classifier::SUBCOMMAND_TERMINATOR; 978 return detail::Classifier::SUBCOMMAND_TERMINATOR;
  979 + auto dotloc = current.find_first_of('.');
  980 + if(dotloc != std::string::npos) {
  981 + auto *cm = _find_subcommand(current.substr(0, dotloc), true, ignore_used_subcommands);
  982 + if(cm != nullptr) {
  983 + auto res = cm->_recognize(current.substr(dotloc + 1), ignore_used_subcommands);
  984 + if(res == detail::Classifier::SUBCOMMAND) {
  985 + return res;
  986 + }
  987 + }
  988 + }
979 return detail::Classifier::NONE; 989 return detail::Classifier::NONE;
980 } 990 }
981 991
@@ -1458,7 +1468,7 @@ CLI11_INLINE bool App::_parse_single(std::vector&lt;std::string&gt; &amp;args, bool &amp;posit @@ -1458,7 +1468,7 @@ CLI11_INLINE bool App::_parse_single(std::vector&lt;std::string&gt; &amp;args, bool &amp;posit
1458 case detail::Classifier::SHORT: 1468 case detail::Classifier::SHORT:
1459 case detail::Classifier::WINDOWS_STYLE: 1469 case detail::Classifier::WINDOWS_STYLE:
1460 // If already parsed a subcommand, don't accept options_ 1470 // If already parsed a subcommand, don't accept options_
1461 - _parse_arg(args, classifier); 1471 + _parse_arg(args, classifier, false);
1462 break; 1472 break;
1463 case detail::Classifier::NONE: 1473 case detail::Classifier::NONE:
1464 // Probably a positional or something for a parent (sub)command 1474 // Probably a positional or something for a parent (sub)command
@@ -1646,6 +1656,17 @@ CLI11_INLINE bool App::_parse_subcommand(std::vector&lt;std::string&gt; &amp;args) { @@ -1646,6 +1656,17 @@ CLI11_INLINE bool App::_parse_subcommand(std::vector&lt;std::string&gt; &amp;args) {
1646 return true; 1656 return true;
1647 } 1657 }
1648 auto *com = _find_subcommand(args.back(), true, true); 1658 auto *com = _find_subcommand(args.back(), true, true);
  1659 + if(com == nullptr) {
  1660 + // the main way to get here is using .notation
  1661 + auto dotloc = args.back().find_first_of('.');
  1662 + if(dotloc != std::string::npos) {
  1663 + com = _find_subcommand(args.back().substr(0, dotloc), true, true);
  1664 + if(com != nullptr) {
  1665 + args.back() = args.back().substr(dotloc + 1);
  1666 + args.push_back(com->get_display_name());
  1667 + }
  1668 + }
  1669 + }
1649 if(com != nullptr) { 1670 if(com != nullptr) {
1650 args.pop_back(); 1671 args.pop_back();
1651 if(!com->silent_) { 1672 if(!com->silent_) {
@@ -1668,7 +1689,8 @@ CLI11_INLINE bool App::_parse_subcommand(std::vector&lt;std::string&gt; &amp;args) { @@ -1668,7 +1689,8 @@ CLI11_INLINE bool App::_parse_subcommand(std::vector&lt;std::string&gt; &amp;args) {
1668 return false; 1689 return false;
1669 } 1690 }
1670 1691
1671 -CLI11_INLINE bool App::_parse_arg(std::vector<std::string> &args, detail::Classifier current_type) { 1692 +CLI11_INLINE bool
  1693 +App::_parse_arg(std::vector<std::string> &args, detail::Classifier current_type, bool local_processing_only) {
1672 1694
1673 std::string current = args.back(); 1695 std::string current = args.back();
1674 1696
@@ -1710,7 +1732,7 @@ CLI11_INLINE bool App::_parse_arg(std::vector&lt;std::string&gt; &amp;args, detail::Classi @@ -1710,7 +1732,7 @@ CLI11_INLINE bool App::_parse_arg(std::vector&lt;std::string&gt; &amp;args, detail::Classi
1710 if(op_ptr == std::end(options_)) { 1732 if(op_ptr == std::end(options_)) {
1711 for(auto &subc : subcommands_) { 1733 for(auto &subc : subcommands_) {
1712 if(subc->name_.empty() && !subc->disabled_) { 1734 if(subc->name_.empty() && !subc->disabled_) {
1713 - if(subc->_parse_arg(args, current_type)) { 1735 + if(subc->_parse_arg(args, current_type, local_processing_only)) {
1714 if(!subc->pre_parse_called_) { 1736 if(!subc->pre_parse_called_) {
1715 subc->_trigger_pre_parse(args.size()); 1737 subc->_trigger_pre_parse(args.size());
1716 } 1738 }
@@ -1724,9 +1746,57 @@ CLI11_INLINE bool App::_parse_arg(std::vector&lt;std::string&gt; &amp;args, detail::Classi @@ -1724,9 +1746,57 @@ CLI11_INLINE bool App::_parse_arg(std::vector&lt;std::string&gt; &amp;args, detail::Classi
1724 return false; 1746 return false;
1725 } 1747 }
1726 1748
  1749 + // now check for '.' notation of subcommands
  1750 + auto dotloc = arg_name.find_first_of('.', 1);
  1751 + if(dotloc != std::string::npos) {
  1752 + // using dot notation is equivalent to single argument subcommand
  1753 + auto *sub = _find_subcommand(arg_name.substr(0, dotloc), true, false);
  1754 + if(sub != nullptr) {
  1755 + auto v = args.back();
  1756 + args.pop_back();
  1757 + arg_name = arg_name.substr(dotloc + 1);
  1758 + if(arg_name.size() > 1) {
  1759 + args.push_back(std::string("--") + v.substr(dotloc + 3));
  1760 + current_type = detail::Classifier::LONG;
  1761 + } else {
  1762 + auto nval = v.substr(dotloc + 2);
  1763 + nval.front() = '-';
  1764 + if(nval.size() > 2) {
  1765 + // '=' not allowed in short form arguments
  1766 + args.push_back(nval.substr(3));
  1767 + nval.resize(2);
  1768 + }
  1769 + args.push_back(nval);
  1770 + current_type = detail::Classifier::SHORT;
  1771 + }
  1772 + auto val = sub->_parse_arg(args, current_type, true);
  1773 + if(val) {
  1774 + if(!sub->silent_) {
  1775 + parsed_subcommands_.push_back(sub);
  1776 + }
  1777 + // deal with preparsing
  1778 + increment_parsed();
  1779 + _trigger_pre_parse(args.size());
  1780 + // run the parse complete callback since the subcommand processing is now complete
  1781 + if(sub->parse_complete_callback_) {
  1782 + sub->_process_env();
  1783 + sub->_process_callbacks();
  1784 + sub->_process_help_flags();
  1785 + sub->_process_requirements();
  1786 + sub->run_callback(false, true);
  1787 + }
  1788 + return true;
  1789 + }
  1790 + args.pop_back();
  1791 + args.push_back(v);
  1792 + }
  1793 + }
  1794 + if(local_processing_only) {
  1795 + return false;
  1796 + }
1727 // If a subcommand, try the main command 1797 // If a subcommand, try the main command
1728 if(parent_ != nullptr && fallthrough_) 1798 if(parent_ != nullptr && fallthrough_)
1729 - return _get_fallthrough_parent()->_parse_arg(args, current_type); 1799 + return _get_fallthrough_parent()->_parse_arg(args, current_type, false);
1730 1800
1731 // Otherwise, add to missing 1801 // Otherwise, add to missing
1732 args.pop_back(); 1802 args.pop_back();
include/CLI/impl/Split_inl.hpp
@@ -34,7 +34,7 @@ CLI11_INLINE bool split_short(const std::string &amp;current, std::string &amp;name, std @@ -34,7 +34,7 @@ CLI11_INLINE bool split_short(const std::string &amp;current, std::string &amp;name, std
34 } 34 }
35 35
36 CLI11_INLINE bool split_long(const std::string &current, std::string &name, std::string &value) { 36 CLI11_INLINE bool split_long(const std::string &current, std::string &name, std::string &value) {
37 - if(current.size() > 2 && current.substr(0, 2) == "--" && valid_first_char(current[2])) { 37 + if(current.size() > 2 && current.compare(0, 2, "--") == 0 && valid_first_char(current[2])) {
38 auto loc = current.find_first_of('='); 38 auto loc = current.find_first_of('=');
39 if(loc != std::string::npos) { 39 if(loc != std::string::npos) {
40 name = current.substr(2, loc - 2); 40 name = current.substr(2, loc - 2);
tests/AppTest.cpp
@@ -2118,21 +2118,23 @@ TEST_CASE_METHOD(TApp, &quot;AllowExtrasArgModify&quot;, &quot;[app]&quot;) { @@ -2118,21 +2118,23 @@ TEST_CASE_METHOD(TApp, &quot;AllowExtrasArgModify&quot;, &quot;[app]&quot;) {
2118 TEST_CASE_METHOD(TApp, "CheckShortFail", "[app]") { 2118 TEST_CASE_METHOD(TApp, "CheckShortFail", "[app]") {
2119 args = {"--two"}; 2119 args = {"--two"};
2120 2120
2121 - CHECK_THROWS_AS(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::SHORT), CLI::HorribleError); 2121 + CHECK_THROWS_AS(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::SHORT, false),
  2122 + CLI::HorribleError);
2122 } 2123 }
2123 2124
2124 // Test horrible error 2125 // Test horrible error
2125 TEST_CASE_METHOD(TApp, "CheckLongFail", "[app]") { 2126 TEST_CASE_METHOD(TApp, "CheckLongFail", "[app]") {
2126 args = {"-t"}; 2127 args = {"-t"};
2127 2128
2128 - CHECK_THROWS_AS(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::LONG), CLI::HorribleError); 2129 + CHECK_THROWS_AS(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::LONG, false),
  2130 + CLI::HorribleError);
2129 } 2131 }
2130 2132
2131 // Test horrible error 2133 // Test horrible error
2132 TEST_CASE_METHOD(TApp, "CheckWindowsFail", "[app]") { 2134 TEST_CASE_METHOD(TApp, "CheckWindowsFail", "[app]") {
2133 args = {"-t"}; 2135 args = {"-t"};
2134 2136
2135 - CHECK_THROWS_AS(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::WINDOWS_STYLE), 2137 + CHECK_THROWS_AS(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::WINDOWS_STYLE, false),
2136 CLI::HorribleError); 2138 CLI::HorribleError);
2137 } 2139 }
2138 2140
@@ -2140,7 +2142,8 @@ TEST_CASE_METHOD(TApp, &quot;CheckWindowsFail&quot;, &quot;[app]&quot;) { @@ -2140,7 +2142,8 @@ TEST_CASE_METHOD(TApp, &quot;CheckWindowsFail&quot;, &quot;[app]&quot;) {
2140 TEST_CASE_METHOD(TApp, "CheckOtherFail", "[app]") { 2142 TEST_CASE_METHOD(TApp, "CheckOtherFail", "[app]") {
2141 args = {"-t"}; 2143 args = {"-t"};
2142 2144
2143 - CHECK_THROWS_AS(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::NONE), CLI::HorribleError); 2145 + CHECK_THROWS_AS(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::NONE, false),
  2146 + CLI::HorribleError);
2144 } 2147 }
2145 2148
2146 // Test horrible error 2149 // Test horrible error
tests/SubcommandTest.cpp
@@ -1410,6 +1410,15 @@ TEST_CASE_METHOD(ManySubcommands, &quot;SubcommandNeedsOptionsCallbackOrdering&quot;, &quot;[su @@ -1410,6 +1410,15 @@ TEST_CASE_METHOD(ManySubcommands, &quot;SubcommandNeedsOptionsCallbackOrdering&quot;, &quot;[su
1410 CHECK_NOTHROW(run()); 1410 CHECK_NOTHROW(run());
1411 } 1411 }
1412 1412
  1413 +TEST_CASE_METHOD(ManySubcommands, "SubcommandParseCompleteDotNotation", "[subcom]") {
  1414 + int count{0};
  1415 + sub1->add_flag("--flag1");
  1416 + sub1->parse_complete_callback([&count]() { ++count; });
  1417 + args = {"--sub1.flag1", "--sub1.flag1"};
  1418 + run();
  1419 + CHECK(count == 2);
  1420 +}
  1421 +
1413 TEST_CASE_METHOD(ManySubcommands, "SubcommandNeedsFail", "[subcom]") { 1422 TEST_CASE_METHOD(ManySubcommands, "SubcommandNeedsFail", "[subcom]") {
1414 1423
1415 auto *opt = app.add_flag("--subactive"); 1424 auto *opt = app.add_flag("--subactive");
@@ -1984,3 +1993,126 @@ TEST_CASE_METHOD(TApp, &quot;SubcommandInOptionGroupCallbackCount&quot;, &quot;[subcom]&quot;) { @@ -1984,3 +1993,126 @@ TEST_CASE_METHOD(TApp, &quot;SubcommandInOptionGroupCallbackCount&quot;, &quot;[subcom]&quot;) {
1984 run(); 1993 run();
1985 CHECK(subcount == 1); 1994 CHECK(subcount == 1);
1986 } 1995 }
  1996 +
  1997 +TEST_CASE_METHOD(TApp, "DotNotationSubcommand", "[subcom]") {
  1998 + std::string v1, v2, vbase;
  1999 +
  2000 + auto *sub1 = app.add_subcommand("sub1");
  2001 + auto *sub2 = app.add_subcommand("sub2");
  2002 + sub1->add_option("--value", v1);
  2003 + sub2->add_option("--value", v2);
  2004 + app.add_option("--value", vbase);
  2005 + args = {"--sub1.value", "val1"};
  2006 + run();
  2007 + CHECK(v1 == "val1");
  2008 +
  2009 + args = {"--sub2.value", "val2", "--value", "base"};
  2010 + run();
  2011 + CHECK(v2 == "val2");
  2012 + CHECK(vbase == "base");
  2013 + v1.clear();
  2014 + v2.clear();
  2015 + vbase.clear();
  2016 +
  2017 + args = {"--sub2.value=val2", "--value=base"};
  2018 + run();
  2019 + CHECK(v2 == "val2");
  2020 + CHECK(vbase == "base");
  2021 +
  2022 + auto subs = app.get_subcommands();
  2023 + REQUIRE(!subs.empty());
  2024 + CHECK(subs.front()->get_name() == "sub2");
  2025 +}
  2026 +
  2027 +TEST_CASE_METHOD(TApp, "DotNotationSubcommandSingleChar", "[subcom]") {
  2028 + std::string v1, v2, vbase;
  2029 +
  2030 + auto *sub1 = app.add_subcommand("sub1");
  2031 + auto *sub2 = app.add_subcommand("sub2");
  2032 + sub1->add_option("-v", v1);
  2033 + sub2->add_option("-v", v2);
  2034 + app.add_option("-v", vbase);
  2035 + args = {"--sub1.v", "val1"};
  2036 + run();
  2037 + CHECK(v1 == "val1");
  2038 +
  2039 + args = {"--sub2.v", "val2", "-v", "base"};
  2040 + run();
  2041 + CHECK(v2 == "val2");
  2042 + CHECK(vbase == "base");
  2043 + v1.clear();
  2044 + v2.clear();
  2045 + vbase.clear();
  2046 +
  2047 + args = {"--sub2.v=val2", "-vbase"};
  2048 + run();
  2049 + CHECK(v2 == "val2");
  2050 + CHECK(vbase == "base");
  2051 +
  2052 + auto subs = app.get_subcommands();
  2053 + REQUIRE(!subs.empty());
  2054 + CHECK(subs.front()->get_name() == "sub2");
  2055 +}
  2056 +
  2057 +TEST_CASE_METHOD(TApp, "DotNotationSubcommandRecusive", "[subcom]") {
  2058 + std::string v1, v2, v3, vbase;
  2059 +
  2060 + auto *sub1 = app.add_subcommand("sub1");
  2061 + auto *sub2 = sub1->add_subcommand("sub2");
  2062 + auto *sub3 = sub2->add_subcommand("sub3");
  2063 +
  2064 + sub1->add_option("--value", v1);
  2065 + sub2->add_option("--value", v2);
  2066 + sub3->add_option("--value", v3);
  2067 + app.add_option("--value", vbase);
  2068 + args = {"--sub1.sub2.sub3.value", "val1"};
  2069 + run();
  2070 + CHECK(v3 == "val1");
  2071 +
  2072 + args = {"--sub1.sub2.value", "val2"};
  2073 + run();
  2074 + CHECK(v2 == "val2");
  2075 +
  2076 + args = {"--sub1.sub2.bob", "val2"};
  2077 + CHECK_THROWS_AS(run(), CLI::ExtrasError);
  2078 + app.allow_extras();
  2079 + CHECK_NOTHROW(run());
  2080 + auto extras = app.remaining();
  2081 + CHECK(extras.size() == 2);
  2082 + CHECK(extras.front() == "--sub1.sub2.bob");
  2083 +}
  2084 +
  2085 +TEST_CASE_METHOD(TApp, "DotNotationSubcommandRecusive2", "[subcom]") {
  2086 + std::string v1, v2, v3, vbase;
  2087 +
  2088 + auto *sub1 = app.add_subcommand("sub1");
  2089 + auto *sub2 = sub1->add_subcommand("sub2");
  2090 + auto *sub3 = sub2->add_subcommand("sub3");
  2091 +
  2092 + sub1->add_option("--value", v1);
  2093 + sub2->add_option("--value", v2);
  2094 + sub3->add_option("--value", v3);
  2095 + app.add_option("--value", vbase);
  2096 + args = {"sub1.sub2.sub3", "--value", "val1"};
  2097 + run();
  2098 + CHECK(v3 == "val1");
  2099 +
  2100 + args = {"sub1.sub2", "--value", "val2"};
  2101 + run();
  2102 + CHECK(v2 == "val2");
  2103 +
  2104 + args = {"sub1.bob", "--value", "val2"};
  2105 + CHECK_THROWS_AS(run(), CLI::ExtrasError);
  2106 +
  2107 + args = {"sub1.sub2.bob", "--value", "val2"};
  2108 + CHECK_THROWS_AS(run(), CLI::ExtrasError);
  2109 +
  2110 + args = {"sub1.sub2.sub3.bob", "--value", "val2"};
  2111 + CHECK_THROWS_AS(run(), CLI::ExtrasError);
  2112 +
  2113 + app.allow_extras();
  2114 + CHECK_NOTHROW(run());
  2115 + auto extras = app.remaining();
  2116 + CHECK(extras.size() == 1);
  2117 + CHECK(extras.front() == "sub1.sub2.sub3.bob");
  2118 +}