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 2 # Note: this is a header only library. If you have an older CMake than 3.4,
3 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 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 8 # built-in and modified CMake 3.11
9 9 if(${CMAKE_VERSION} VERSION_LESS 3.24)
10 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 938 triggered if any options from the subcommand were parsed. Subcommand names given
939 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 955 #### Subcommand options
942 956  
943 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 1285 bool _parse_subcommand(std::vector<std::string> &args);
1286 1286  
1287 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 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 1292 /// Trigger the pre_parse callback if needed
1292 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 976 return detail::Classifier::WINDOWS_STYLE;
977 977 if((current == "++") && !name_.empty() && parent_ != nullptr)
978 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 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 1468 case detail::Classifier::SHORT:
1459 1469 case detail::Classifier::WINDOWS_STYLE:
1460 1470 // If already parsed a subcommand, don't accept options_
1461   - _parse_arg(args, classifier);
  1471 + _parse_arg(args, classifier, false);
1462 1472 break;
1463 1473 case detail::Classifier::NONE:
1464 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 1656 return true;
1647 1657 }
1648 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 1670 if(com != nullptr) {
1650 1671 args.pop_back();
1651 1672 if(!com->silent_) {
... ... @@ -1668,7 +1689,8 @@ CLI11_INLINE bool App::_parse_subcommand(std::vector&lt;std::string&gt; &amp;args) {
1668 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 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 1732 if(op_ptr == std::end(options_)) {
1711 1733 for(auto &subc : subcommands_) {
1712 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 1736 if(!subc->pre_parse_called_) {
1715 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 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 1797 // If a subcommand, try the main command
1728 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 1801 // Otherwise, add to missing
1732 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 34 }
35 35  
36 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 38 auto loc = current.find_first_of('=');
39 39 if(loc != std::string::npos) {
40 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 2118 TEST_CASE_METHOD(TApp, "CheckShortFail", "[app]") {
2119 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 2125 // Test horrible error
2125 2126 TEST_CASE_METHOD(TApp, "CheckLongFail", "[app]") {
2126 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 2133 // Test horrible error
2132 2134 TEST_CASE_METHOD(TApp, "CheckWindowsFail", "[app]") {
2133 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 2138 CLI::HorribleError);
2137 2139 }
2138 2140  
... ... @@ -2140,7 +2142,8 @@ TEST_CASE_METHOD(TApp, &quot;CheckWindowsFail&quot;, &quot;[app]&quot;) {
2140 2142 TEST_CASE_METHOD(TApp, "CheckOtherFail", "[app]") {
2141 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 2149 // Test horrible error
... ...
tests/SubcommandTest.cpp
... ... @@ -1410,6 +1410,15 @@ TEST_CASE_METHOD(ManySubcommands, &quot;SubcommandNeedsOptionsCallbackOrdering&quot;, &quot;[su
1410 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 1422 TEST_CASE_METHOD(ManySubcommands, "SubcommandNeedsFail", "[subcom]") {
1414 1423  
1415 1424 auto *opt = app.add_flag("--subactive");
... ... @@ -1984,3 +1993,126 @@ TEST_CASE_METHOD(TApp, &quot;SubcommandInOptionGroupCallbackCount&quot;, &quot;[subcom]&quot;) {
1984 1993 run();
1985 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 +}
... ...