Commit 4c7c8ddc45d2ef74584e5cd945f7a4d27c987748
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>
Showing
7 changed files
with
232 additions
and
12 deletions
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<std::string> &args, bool &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<std::string> &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<std::string> &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<std::string> &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<std::string> &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 &current, std::string &name, std |
| 34 | 34 | } |
| 35 | 35 | |
| 36 | 36 | CLI11_INLINE bool split_long(const std::string ¤t, 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, "AllowExtrasArgModify", "[app]") { |
| 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, "CheckWindowsFail", "[app]") { |
| 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, "SubcommandNeedsOptionsCallbackOrdering", "[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, "SubcommandInOptionGroupCallbackCount", "[subcom]") { |
| 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 | +} | ... | ... |