Commit e976f964c3ee6a4f6faa836ecb38f3ff8b043114

Authored by Eyal Rozenberg
Committed by GitHub
1 parent 43ebb494

Fix issue #240: Multiple long option names / aliases (#349)

* Fixes #240: Multiple long option names / aliases

* We now use a vector of long option names instead of a single name
* When specifying an option, you can provide multiple names separated by commas, at most one of which may have a length of 1 (not necessarily the first specified name). The length-1 name is the single-hyphen switch (the "short name").
* Hashing uses the first long name
* Option help currently only uses the first long name.
CHANGELOG.md
... ... @@ -7,6 +7,7 @@ options. The project adheres to semantic versioning.
7 7  
8 8 ### Added
9 9  
  10 +* Support for multiple long names for the same option (= multiple long aliases)
10 11 * Add a `program()` function to retrieve the program name.
11 12 * Added a .clang-format file.
12 13  
... ...
include/cxxopts.hpp
... ... @@ -27,6 +27,7 @@ THE SOFTWARE.
27 27 #ifndef CXXOPTS_HPP_INCLUDED
28 28 #define CXXOPTS_HPP_INCLUDED
29 29  
  30 +#include <cassert>
30 31 #include <cctype>
31 32 #include <cstring>
32 33 #include <exception>
... ... @@ -101,6 +102,7 @@ static constexpr struct {
101 102 #include <unicode/unistr.h>
102 103  
103 104 namespace cxxopts {
  105 +
104 106 using String = icu::UnicodeString;
105 107  
106 108 inline
... ... @@ -248,6 +250,7 @@ end(const icu::UnicodeString&amp; s)
248 250 #else
249 251  
250 252 namespace cxxopts {
  253 +
251 254 using String = std::string;
252 255  
253 256 template <typename T>
... ... @@ -522,6 +525,7 @@ class incorrect_argument_type : public parsing
522 525  
523 526 } // namespace exceptions
524 527  
  528 +
525 529 template <typename T>
526 530 void throw_or_mimic(const std::string& text)
527 531 {
... ... @@ -541,6 +545,8 @@ void throw_or_mimic(const std::string&amp; text)
541 545 #endif
542 546 }
543 547  
  548 +using OptionNames = std::vector<std::string>;
  549 +
544 550 namespace values {
545 551  
546 552 namespace parser_tool {
... ... @@ -624,28 +630,44 @@ inline bool IsFalseText(const std::string &amp;text)
624 630 return false;
625 631 }
626 632  
627   -inline std::pair<std::string, std::string> SplitSwitchDef(const std::string &text)
  633 +inline OptionNames split_option_names(const std::string &text)
628 634 {
629   - std::string short_sw, long_sw;
630   - const char *pdata = text.c_str();
631   - if (isalnum(*pdata) && *(pdata + 1) == ',') {
632   - short_sw = std::string(1, *pdata);
633   - pdata += 2;
634   - }
635   - while (*pdata == ' ') { pdata += 1; }
636   - if (isalnum(*pdata)) {
637   - const char *store = pdata;
638   - pdata += 1;
639   - while (isalnum(*pdata) || *pdata == '-' || *pdata == '_') {
640   - pdata += 1;
  635 + OptionNames split_names;
  636 +
  637 + std::string::size_type token_start_pos = 0;
  638 + auto length = text.length();
  639 +
  640 + while (token_start_pos < length) {
  641 + const auto &npos = std::string::npos;
  642 + auto next_non_space_pos = text.find_first_not_of(' ', token_start_pos);
  643 + if (next_non_space_pos == npos) {
  644 + throw_or_mimic<exceptions::invalid_option_format>(text);
641 645 }
642   - if (*pdata == '\0') {
643   - long_sw = std::string(store, pdata - store);
644   - } else {
  646 + token_start_pos = next_non_space_pos;
  647 + auto next_delimiter_pos = text.find(',', token_start_pos);
  648 + if (next_delimiter_pos == token_start_pos) {
645 649 throw_or_mimic<exceptions::invalid_option_format>(text);
646 650 }
  651 + if (next_delimiter_pos == npos) {
  652 + next_delimiter_pos = length;
  653 + }
  654 + auto token_length = next_delimiter_pos - token_start_pos;
  655 + // validate the token itself matches the regex /([:alnum:][-_[:alnum:]]*/
  656 + {
  657 + const char* option_name_valid_chars =
  658 + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  659 + "abcdefghijklmnopqrstuvwxyz"
  660 + "0123456789"
  661 + "_-";
  662 + if (!std::isalnum(text[token_start_pos]) ||
  663 + text.find_first_not_of(option_name_valid_chars, token_start_pos) < next_delimiter_pos) {
  664 + throw_or_mimic<exceptions::invalid_option_format>(text);
  665 + }
  666 + }
  667 + split_names.emplace_back(text.substr(token_start_pos, token_length));
  668 + token_start_pos = next_delimiter_pos + 1;
647 669 }
648   - return std::pair<std::string, std::string>(short_sw, long_sw);
  670 + return split_names;
649 671 }
650 672  
651 673 inline ArguDesc ParseArgument(const char *arg, bool &matched)
... ... @@ -712,7 +734,8 @@ std::basic_regex&lt;char&gt; falsy_pattern
712 734 std::basic_regex<char> option_matcher
713 735 ("--([[:alnum:]][-_[:alnum:]]+)(=(.*))?|-([[:alnum:]]+)");
714 736 std::basic_regex<char> option_specifier
715   - ("(([[:alnum:]]),)?[ ]*([[:alnum:]][-_[:alnum:]]*)?");
  737 + ("([[:alnum:]][-_[:alnum:]]*)(,[ ]*[[:alnum:]][-_[:alnum:]]*)*");
  738 +std::basic_regex<char> option_specifier_separator(", *");
716 739  
717 740 } // namespace
718 741  
... ... @@ -755,19 +778,23 @@ inline bool IsFalseText(const std::string &amp;text)
755 778 return !result.empty();
756 779 }
757 780  
758   -inline std::pair<std::string, std::string> SplitSwitchDef(const std::string &text)
  781 +// Gets the option names specified via a single, comma-separated string,
  782 +// and returns the separate, space-discarded, non-empty names
  783 +// (without considering which or how many are single-character)
  784 +inline OptionNames split_option_names(const std::string &text)
759 785 {
760   - std::match_results<const char*> result;
761   - std::regex_match(text.c_str(), result, option_specifier);
762   - if (result.empty())
  786 + if (!std::regex_match(text.c_str(), option_specifier))
763 787 {
764 788 throw_or_mimic<exceptions::invalid_option_format>(text);
765 789 }
766 790  
767   - const std::string& short_sw = result[2];
768   - const std::string& long_sw = result[3];
  791 + OptionNames split_names;
769 792  
770   - return std::pair<std::string, std::string>(short_sw, long_sw);
  793 + constexpr int use_non_matches { -1 };
  794 + auto token_iterator = std::sregex_token_iterator(
  795 + text.begin(), text.end(), option_specifier_separator, use_non_matches);
  796 + std::copy(token_iterator, std::sregex_token_iterator(), std::back_inserter(split_names));
  797 + return split_names;
771 798 }
772 799  
773 800 inline ArguDesc ParseArgument(const char *arg, bool &matched)
... ... @@ -1221,13 +1248,22 @@ value(T&amp; t)
1221 1248  
1222 1249 class OptionAdder;
1223 1250  
  1251 +inline
  1252 +CXXOPTS_NODISCARD
  1253 +const std::string&
  1254 +first_or_empty(const OptionNames& long_names)
  1255 +{
  1256 + static const std::string empty{""};
  1257 + return long_names.empty() ? empty : long_names.front();
  1258 +}
  1259 +
1224 1260 class OptionDetails
1225 1261 {
1226 1262 public:
1227 1263 OptionDetails
1228 1264 (
1229 1265 std::string short_,
1230   - std::string long_,
  1266 + OptionNames long_,
1231 1267 String desc,
1232 1268 std::shared_ptr<const Value> val
1233 1269 )
... ... @@ -1237,7 +1273,7 @@ class OptionDetails
1237 1273 , m_value(std::move(val))
1238 1274 , m_count(0)
1239 1275 {
1240   - m_hash = std::hash<std::string>{}(m_long + m_short);
  1276 + m_hash = std::hash<std::string>{}(first_long_name() + m_short);
1241 1277 }
1242 1278  
1243 1279 OptionDetails(const OptionDetails& rhs)
... ... @@ -1278,16 +1314,23 @@ class OptionDetails
1278 1314  
1279 1315 CXXOPTS_NODISCARD
1280 1316 const std::string&
1281   - long_name() const
  1317 + first_long_name() const
1282 1318 {
1283   - return m_long;
  1319 + return first_or_empty(m_long);
1284 1320 }
1285 1321  
1286 1322 CXXOPTS_NODISCARD
1287 1323 const std::string&
1288 1324 essential_name() const
1289 1325 {
1290   - return m_long.empty() ? m_short : m_long;
  1326 + return m_long.empty() ? m_short : m_long.front();
  1327 + }
  1328 +
  1329 + CXXOPTS_NODISCARD
  1330 + const OptionNames &
  1331 + long_names() const
  1332 + {
  1333 + return m_long;
1291 1334 }
1292 1335  
1293 1336 size_t
... ... @@ -1298,7 +1341,7 @@ class OptionDetails
1298 1341  
1299 1342 private:
1300 1343 std::string m_short{};
1301   - std::string m_long{};
  1344 + OptionNames m_long{};
1302 1345 String m_desc{};
1303 1346 std::shared_ptr<const Value> m_value{};
1304 1347 int m_count;
... ... @@ -1309,7 +1352,7 @@ class OptionDetails
1309 1352 struct HelpOptionDetails
1310 1353 {
1311 1354 std::string s;
1312   - std::string l;
  1355 + OptionNames l;
1313 1356 String desc;
1314 1357 bool has_default;
1315 1358 std::string default_value;
... ... @@ -1340,7 +1383,7 @@ class OptionValue
1340 1383 ensure_value(details);
1341 1384 ++m_count;
1342 1385 m_value->parse(text);
1343   - m_long_name = &details->long_name();
  1386 + m_long_names = &details->long_names();
1344 1387 }
1345 1388  
1346 1389 void
... ... @@ -1348,14 +1391,14 @@ class OptionValue
1348 1391 {
1349 1392 ensure_value(details);
1350 1393 m_default = true;
1351   - m_long_name = &details->long_name();
  1394 + m_long_names = &details->long_names();
1352 1395 m_value->parse();
1353 1396 }
1354 1397  
1355 1398 void
1356 1399 parse_no_value(const std::shared_ptr<const OptionDetails>& details)
1357 1400 {
1358   - m_long_name = &details->long_name();
  1401 + m_long_names = &details->long_names();
1359 1402 }
1360 1403  
1361 1404 #if defined(CXXOPTS_NULL_DEREF_IGNORE)
... ... @@ -1388,7 +1431,7 @@ class OptionValue
1388 1431 {
1389 1432 if (m_value == nullptr) {
1390 1433 throw_or_mimic<exceptions::option_has_no_value>(
1391   - m_long_name == nullptr ? "" : *m_long_name);
  1434 + m_long_names == nullptr ? "" : first_or_empty(*m_long_names));
1392 1435 }
1393 1436  
1394 1437 #ifdef CXXOPTS_NO_RTTI
... ... @@ -1409,7 +1452,7 @@ class OptionValue
1409 1452 }
1410 1453  
1411 1454  
1412   - const std::string* m_long_name = nullptr;
  1455 + const OptionNames * m_long_names = nullptr;
1413 1456 // Holding this pointer is safe, since OptionValue's only exist in key-value pairs,
1414 1457 // where the key has the string we point to.
1415 1458 std::shared_ptr<Value> m_value{};
... ... @@ -1797,12 +1840,28 @@ class Options
1797 1840 (
1798 1841 const std::string& group,
1799 1842 const std::string& s,
1800   - const std::string& l,
  1843 + const OptionNames& l,
1801 1844 std::string desc,
1802 1845 const std::shared_ptr<const Value>& value,
1803 1846 std::string arg_help
1804 1847 );
1805 1848  
  1849 + void
  1850 + add_option
  1851 + (
  1852 + const std::string& group,
  1853 + const std::string& short_name,
  1854 + const std::string& single_long_name,
  1855 + std::string desc,
  1856 + const std::shared_ptr<const Value>& value,
  1857 + std::string arg_help
  1858 + )
  1859 + {
  1860 + OptionNames long_names;
  1861 + long_names.emplace_back(single_long_name);
  1862 + add_option(group, short_name, long_names, desc, value, arg_help);
  1863 + }
  1864 +
1806 1865 //parse positional arguments into the given option
1807 1866 void
1808 1867 parse_positional(std::string option);
... ... @@ -1897,7 +1956,6 @@ class OptionAdder
1897 1956 };
1898 1957  
1899 1958 namespace {
1900   -
1901 1959 constexpr size_t OPTION_LONGEST = 30;
1902 1960 constexpr size_t OPTION_DESC_GAP = 2;
1903 1961  
... ... @@ -1908,7 +1966,7 @@ format_option
1908 1966 )
1909 1967 {
1910 1968 const auto& s = o.s;
1911   - const auto& l = o.l;
  1969 + const auto& l = first_or_empty(o.l);
1912 1970  
1913 1971 String result = " ";
1914 1972  
... ... @@ -2111,36 +2169,30 @@ OptionAdder::operator()
2111 2169 std::string arg_help
2112 2170 )
2113 2171 {
2114   - std::string short_sw, long_sw;
2115   - std::tie(short_sw, long_sw) = values::parser_tool::SplitSwitchDef(opts);
2116   -
2117   - if (!short_sw.length() && !long_sw.length())
2118   - {
  2172 + OptionNames option_names = values::parser_tool::split_option_names(opts);
  2173 + // Note: All names will be non-empty; but we must separate the short
  2174 + // (length-1) and longer names
  2175 + std::string short_name {""};
  2176 + auto first_short_name_iter =
  2177 + std::partition(option_names.begin(), option_names.end(),
  2178 + [&](const std::string& name) { return name.length() > 1; }
  2179 + );
  2180 + auto num_length_1_names = (option_names.end() - first_short_name_iter);
  2181 + switch(num_length_1_names) {
  2182 + case 1:
  2183 + short_name = *first_short_name_iter;
  2184 + option_names.erase(first_short_name_iter);
  2185 + case 0:
  2186 + break;
  2187 + default:
2119 2188 throw_or_mimic<exceptions::invalid_option_format>(opts);
2120   - }
2121   - else if (long_sw.length() == 1 && short_sw.length())
2122   - {
2123   - throw_or_mimic<exceptions::invalid_option_format>(opts);
2124   - }
2125   -
2126   - auto option_names = []
2127   - (
2128   - const std::string &short_,
2129   - const std::string &long_
2130   - )
2131   - {
2132   - if (long_.length() == 1)
2133   - {
2134   - return std::make_tuple(long_, short_);
2135   - }
2136   - return std::make_tuple(short_, long_);
2137   - }(short_sw, long_sw);
  2189 + };
2138 2190  
2139 2191 m_options.add_option
2140 2192 (
2141 2193 m_group,
2142   - std::get<0>(option_names),
2143   - std::get<1>(option_names),
  2194 + short_name,
  2195 + option_names,
2144 2196 desc,
2145 2197 value,
2146 2198 std::move(arg_help)
... ... @@ -2467,7 +2519,9 @@ OptionParser::finalise_aliases()
2467 2519 auto& detail = *option.second;
2468 2520 auto hash = detail.hash();
2469 2521 m_keys[detail.short_name()] = hash;
2470   - m_keys[detail.long_name()] = hash;
  2522 + for(const auto& long_name : detail.long_names()) {
  2523 + m_keys[long_name] = hash;
  2524 + }
2471 2525  
2472 2526 m_parsed.emplace(hash, OptionValue());
2473 2527 }
... ... @@ -2490,7 +2544,7 @@ Options::add_option
2490 2544 (
2491 2545 const std::string& group,
2492 2546 const std::string& s,
2493   - const std::string& l,
  2547 + const OptionNames& l,
2494 2548 std::string desc,
2495 2549 const std::shared_ptr<const Value>& value,
2496 2550 std::string arg_help
... ... @@ -2504,9 +2558,8 @@ Options::add_option
2504 2558 add_one_option(s, option);
2505 2559 }
2506 2560  
2507   - if (!l.empty())
2508   - {
2509   - add_one_option(l, option);
  2561 + for(const auto& long_name : l) {
  2562 + add_one_option(long_name, option);
2510 2563 }
2511 2564  
2512 2565 //add the help details
... ... @@ -2561,7 +2614,8 @@ Options::help_one_group(const std::string&amp; g) const
2561 2614  
2562 2615 for (const auto& o : group->second.options)
2563 2616 {
2564   - if (m_positional_set.find(o.l) != m_positional_set.end() &&
  2617 + assert(!o.l.empty());
  2618 + if (m_positional_set.find(o.l.front()) != m_positional_set.end() &&
2565 2619 !m_show_positional)
2566 2620 {
2567 2621 continue;
... ... @@ -2583,7 +2637,8 @@ Options::help_one_group(const std::string&amp; g) const
2583 2637 auto fiter = format.begin();
2584 2638 for (const auto& o : group->second.options)
2585 2639 {
2586   - if (m_positional_set.find(o.l) != m_positional_set.end() &&
  2640 + assert(!o.l.empty());
  2641 + if (m_positional_set.find(o.l.front()) != m_positional_set.end() &&
2587 2642 !m_show_positional)
2588 2643 {
2589 2644 continue;
... ...
src/example.cpp
... ... @@ -44,7 +44,7 @@ parse(int argc, const char* argv[])
44 44 .set_tab_expansion()
45 45 .allow_unrecognised_options()
46 46 .add_options()
47   - ("a,apple", "an apple", cxxopts::value<bool>(apple))
  47 + ("a,apple,ringo", "an apple", cxxopts::value<bool>(apple))
48 48 ("b,bob", "Bob")
49 49 ("char", "A character", cxxopts::value<char>())
50 50 ("t,true", "True", cxxopts::value<bool>()->default_value("true"))
... ...
test/options.cpp
... ... @@ -49,6 +49,9 @@ TEST_CASE(&quot;Basic options&quot;, &quot;[options]&quot;)
49 49 options.add_options()
50 50 ("long", "a long option")
51 51 ("s,short", "a short option")
  52 + ("quick,brown", "An option with multiple long names and no short name")
  53 + ("f,ox,jumped", "An option with multiple long names and a short name")
  54 + ("over,z,lazy,dog", "An option with multiple long names and a short name, not listed first")
52 55 ("value", "an option with a value", cxxopts::value<std::string>())
53 56 ("a,av", "a short option with a value", cxxopts::value<std::string>())
54 57 ("6,six", "a short number option")
... ... @@ -67,6 +70,14 @@ TEST_CASE(&quot;Basic options&quot;, &quot;[options]&quot;)
67 70 "-6",
68 71 "-p",
69 72 "--space",
  73 + "--quick",
  74 + "--ox",
  75 + "-f",
  76 + "--brown",
  77 + "-z",
  78 + "--over",
  79 + "--dog",
  80 + "--lazy"
70 81 });
71 82  
72 83 auto** actual_argv = argv.argv();
... ... @@ -83,9 +94,12 @@ TEST_CASE(&quot;Basic options&quot;, &quot;[options]&quot;)
83 94 CHECK(result.count("6") == 1);
84 95 CHECK(result.count("p") == 2);
85 96 CHECK(result.count("space") == 2);
  97 + CHECK(result.count("quick") == 2);
  98 + CHECK(result.count("f") == 2);
  99 + CHECK(result.count("z") == 4);
86 100  
87 101 auto& arguments = result.arguments();
88   - REQUIRE(arguments.size() == 7);
  102 + REQUIRE(arguments.size() == 15);
89 103 CHECK(arguments[0].key() == "long");
90 104 CHECK(arguments[0].value() == "true");
91 105 CHECK(arguments[0].as<bool>() == true);
... ... @@ -786,24 +800,30 @@ TEST_CASE(&quot;Option add with add_option(string, Option)&quot;, &quot;[options]&quot;) {
786 800  
787 801 options.add_option("", option_1);
788 802 options.add_option("TEST", {"a,aggregate", "test option 2", cxxopts::value<int>(), "AGGREGATE"});
  803 + options.add_option("TEST", {"multilong,m,multilong-alias", "test option 3", cxxopts::value<int>(), "An option with multiple long names"});
789 804  
790 805 Argv argv_({
791 806 "test",
792 807 "--test",
793 808 "5",
794 809 "-a",
795   - "4"
  810 + "4",
  811 + "--multilong-alias",
  812 + "6"
796 813 });
797 814 auto argc = argv_.argc();
798 815 auto** argv = argv_.argv();
799 816 auto result = options.parse(argc, argv);
800 817  
801   - CHECK(result.arguments().size()==2);
  818 + CHECK(result.arguments().size() == 3);
802 819 CHECK(options.groups().size() == 2);
803 820 CHECK(result.count("address") == 0);
804 821 CHECK(result.count("aggregate") == 1);
805 822 CHECK(result.count("test") == 1);
806 823 CHECK(result["aggregate"].as<int>() == 4);
  824 + CHECK(result["multilong"].as<int>() == 6);
  825 + CHECK(result["multilong-alias"].as<int>() == 6);
  826 + CHECK(result["m"].as<int>() == 6);
807 827 CHECK(result["test"].as<int>() == 5);
808 828 }
809 829  
... ...