From a0f13a15286ca567161090f290c4aadd40ef007a Mon Sep 17 00:00:00 2001 From: Nitin Kumar <59679977+lazysegtree@users.noreply.github.com> Date: Mon, 2 Feb 2026 12:07:12 +0530 Subject: [PATCH] fix: Fix short options parsing with equal sign (#488) * feat: Add test for short options parsing * fix: Update option parsing to allow = with short options * fix: Fix custom option parsing in case of CXXOPTS_NO_REGEX --- include/cxxopts.hpp | 50 +++++++++++++++--- test/options.cpp | 124 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 8 deletions(-) diff --git a/include/cxxopts.hpp b/include/cxxopts.hpp index 537b2e7..1a50874 100644 --- a/include/cxxopts.hpp +++ b/include/cxxopts.hpp @@ -758,8 +758,16 @@ inline ArguDesc ParseArgument(const char *arg, bool &matched) { pdata += 1; if(isalnum(*pdata, std::locale::classic())) { + // If we have '=' right after first alnum, its a match. + if(*(pdata+1) == '=') { + argu_desc.arg_name.push_back(*pdata); + argu_desc.set_value = true; + argu_desc.value = std::string(pdata+2); + } + else{ + argu_desc.arg_name = std::string(pdata); + } argu_desc.grouping = true; - argu_desc.arg_name = std::string(pdata); matched = true; } } @@ -780,7 +788,20 @@ const char* const falsy_pattern = "(f|F)(alse)?|0"; CXXOPTS_LINKONCE const char* const option_pattern = - "--([[:alnum:]][-_[:alnum:]\\.]+)(=(.*))?|-([[:alnum:]].*)"; + "--([[:alnum:]][-_[:alnum:]\\.]+)(=(.*))?|-([[:alnum:]])((=(.*))|(.*))"; +// <-------Long Option--------------------> <-----Short Option-------> +// Groups : +// <---------1------------------><--2--> <--4--------><-----5------> +// <-3> <--6--> <-8> +// <-7> +const int LONG_NAME_IDX=1; +const int LONG_MATCH_IDX=2; +const int LONG_MATCH_VALUE_IDX=3; +const int SHORT_NAME_IDX=4; +const int SHORT_MATCH_IDX=6; +const int SHORT_MATCH_VALUE_IDX=7; +const int SHORT_GROUPING_IDX=8; + CXXOPTS_LINKONCE const char* const option_specifier_pattern = "([[:alnum:]][-_[:alnum:]\\.]*)(,[ ]*[[:alnum:]][-_[:alnum:]]*)*"; @@ -862,13 +883,21 @@ inline ArguDesc ParseArgument(const char *arg, bool &matched) ArguDesc argu_desc; if (matched) { - argu_desc.arg_name = result[1].str(); - argu_desc.set_value = result[2].length() > 0; - argu_desc.value = result[3].str(); - if (result[4].length() > 0) + if(result[LONG_NAME_IDX].length() > 0) { + argu_desc.arg_name = result[LONG_NAME_IDX].str(); + argu_desc.set_value = result[LONG_MATCH_IDX].length() > 0; + argu_desc.value = result[LONG_MATCH_VALUE_IDX].str(); + } + else if (result[SHORT_NAME_IDX].length() > 0) { argu_desc.grouping = true; - argu_desc.arg_name = result[4].str(); + argu_desc.arg_name = result[SHORT_NAME_IDX].str(); + if(result[SHORT_MATCH_IDX].length() > 0){ + argu_desc.set_value = true; + argu_desc.value = result[SHORT_MATCH_VALUE_IDX].str(); + } else { + argu_desc.arg_name += result[SHORT_GROUPING_IDX].str(); + } } } @@ -2579,7 +2608,12 @@ OptionParser::parse(int argc, const char* const* argv) if (i + 1 == s.size()) { //it must be the last argument - checked_parse_arg(argc, argv, current, value, name); + if (argu_desc.set_value) { + parse_option(value, name, argu_desc.value); + } + else{ + checked_parse_arg(argc, argv, current, value, name); + } } else if (value->value().has_implicit()) { diff --git a/test/options.cpp b/test/options.cpp index 6faa438..7ef5aca 100644 --- a/test/options.cpp +++ b/test/options.cpp @@ -153,6 +153,130 @@ TEST_CASE("Short options", "[options]") cxxopts::exceptions::invalid_option_format); } +TEST_CASE("Providing options values via equal sign", "[options]") +{ + cxxopts::Options options("test_equal_sign", " - Providing options values via equal sign"); + + options.add_options() + ("o,option", "a option", cxxopts::value()) + ("b", "bool option as string", + cxxopts::value()->implicit_value("true")->default_value("false")) + ("c", "c option", cxxopts::value()->implicit_value("implicit")); + + struct testcases + { + std::string name; + Argv argv; + std::vector> expValues; + bool parseException; + } tests[] = { + { + "Short value with =", + Argv{"test_equal_sign", "-o=hi", "-b=true"}, + {{"o", "hi"}, {"b", "true"}}, + false + }, + { + "Short non alphanumeric value with =", + Argv{"test_equal_sign", "-o==*hi_$&"}, + {{"o", "=*hi_$&"}, {"b", "false"}}, + false + }, + { + "Short value with = in the value, not as seperator", + Argv{"test_equal_sign", "-oDEBUG=1"}, + {{"o", "DEBUG=1"}}, + false + }, + { + "Invalid short name with =", + Argv{"test_equal_sign", "-?=hi"}, + {}, + true + }, + { + "Short value without =", + Argv{"test_equal_sign", "-ohi"}, + {{"o", "hi"}}, + false + }, + { + "Short empty value", + Argv{"test_equal_sign", "-o="}, + {{"o", ""}}, + false + }, + { + "Short value grouped", + Argv{"test_equal_sign", "-bo", "hi"}, + {{"o", "hi"}, {"b", "true"}}, + false + }, + { + "Multiple short values", + Argv{"test_equal_sign", "-o=hi", "-c=bye"}, + {{"o", "hi"}, {"c", "bye"}}, + false + }, + { + "Multiple short values", + Argv{"test_equal_sign", "-o=hi", "-bc"}, + {{"o", "hi"}, {"c", "implicit"}}, + false + }, + { + "Grouped short values with implicit value", + Argv{"test_equal_sign", "-cbo", "hi"}, + {{"o", "hi"}, {"c", "implicit"}, {"b", "true"}}, + false + }, + { + "Grouped short values with implicit value with =", + Argv{"test_equal_sign", "-cbo=hi"}, + {{"o", "=hi"}, {"c", "implicit"}, {"b", "true"}}, + false + }, + { + "Explicit value for implicit valued options (Parse Expection)", + Argv{"test_equal_sign", "-cX"}, + {}, + true + }, + { + "Long value with equal", + Argv{"test_equal_sign", "--option=hi"}, + {{"o", "hi"}}, + false + }, + { + "Long value without equal", + Argv{"test_equal_sign", "--option", "hi"}, + {{"o", "hi"}}, + false + }, + { + "Long empty value", + Argv{"test_equal_sign", "--option="}, + {{"o", ""}}, + false + }, + }; + + for(const auto& tc : tests) { + SECTION(tc.name){ + if(tc.parseException) { + CHECK_THROWS(options.parse(tc.argv.argc(), tc.argv.argv())); + continue; + } + auto result = options.parse(tc.argv.argc(), tc.argv.argv()); + for (const auto& p : tc.expValues) { + // TODO: Allow the type to be defined in the testcase + CHECK(result[p.first].as() == p.second); + } + } + } +} + TEST_CASE("No positional", "[positional]") { cxxopts::Options options("test_no_positional",