From 66730967aafb7f99ad4031bd6c363bcd25aa13f2 Mon Sep 17 00:00:00 2001 From: Pranav Srinivas Kumar Date: Wed, 21 Sep 2022 09:48:48 -0700 Subject: [PATCH 1/3] Added support for custom prefix characters #67 --- README.md | 47 +++++++++++++ include/argparse/argparse.hpp | 129 ++++++++++++++++++++++++++-------- test/CMakeLists.txt | 1 + test/test_prefix_chars.cpp | 41 +++++++++++ 4 files changed, 187 insertions(+), 31 deletions(-) create mode 100644 test/test_prefix_chars.cpp diff --git a/README.md b/README.md index 7093f0b..82f47ed 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ * [Parent Parsers](#parent-parsers) * [Subcommands](#subcommands) * [Parse Known Args](#parse-known-args) + * [Custom Prefix Characters](#custom-prefix-characters) * [Further Examples](#further-examples) * [Construct a JSON object from a filename argument](#construct-a-json-object-from-a-filename-argument) * [Positional Arguments with Compound Toggle Arguments](#positional-arguments-with-compound-toggle-arguments) @@ -823,6 +824,52 @@ int main(int argc, char *argv[]) { } ``` +### Custom Prefix Characters + +Most command-line options will use `-` as the prefix, e.g. `-f/--foo`. Parsers that need to support different or additional prefix characters, e.g. for options like `+f` or `/foo`, may specify them using the `set_prefix_chars()`: + +```cpp +#include +#include + +int main(int argc, char *argv[]) { + argparse::ArgumentParser program("test"); + program.set_prefix_chars("-+/"); + + program.add_argument("+f"); + program.add_argument("--bar"); + program.add_argument("/foo"); + + try { + program.parse_args(argc, argv); + } + catch (const std::runtime_error& err) { + std::cerr << err.what() << std::endl; + std::cerr << program; + std::exit(1); + } + + if (program.is_used("+f")) { + std::cout << "+f : " << program.get("+f") << "\n"; + } + + if (program.is_used("--bar")) { + std::cout << "--bar : " << program.get("--bar") << "\n"; + } + + if (program.is_used("/foo")) { + std::cout << "/foo : " << program.get("/foo") << "\n"; + } +} +``` + +```console +foo@bar:/home/dev/$ ./main +f 5 --bar 3.14f /foo "Hello" ++f : 5 +--bar : 3.14f +/foo : Hello +``` + ## Further Examples ### Construct a JSON object from a filename argument diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index c0e13dd..b0a1076 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -355,10 +355,12 @@ class Argument { -> std::ostream &; template - explicit Argument(std::array &&a, + explicit Argument(std::string_view prefix_chars, + std::array &&a, std::index_sequence /*unused*/) - : m_is_optional((is_optional(a[I]) || ...)), m_is_required(false), - m_is_repeatable(false), m_is_used(false) { + : m_is_optional((is_optional(a[I], prefix_chars) || ...)), + m_is_required(false), m_is_repeatable(false), m_is_used(false), + m_prefix_chars(prefix_chars) { ((void)m_names.emplace_back(a[I]), ...); std::sort( m_names.begin(), m_names.end(), [](const auto &lhs, const auto &rhs) { @@ -368,8 +370,9 @@ class Argument { public: template - explicit Argument(std::array &&a) - : Argument(std::move(a), std::make_index_sequence{}) {} + explicit Argument(std::string_view prefix_chars, + std::array &&a) + : Argument(prefix_chars, std::move(a), std::make_index_sequence{}) {} Argument &help(std::string help_text) { m_help = std::move(help_text); @@ -513,7 +516,9 @@ public: num_args_max)); } if (!m_accepts_optional_like_value) { - end = std::find_if(start, end, Argument::is_optional); + end = std::find_if( + start, end, + std::bind(is_optional, std::placeholders::_1, m_prefix_chars)); dist = static_cast(std::distance(start, end)); if (dist < num_args_min) { throw std::runtime_error("Too few arguments"); @@ -821,8 +826,9 @@ private: return false; } - static bool is_optional(std::string_view name) { - return !is_positional(name); + static bool is_optional(std::string_view name, + std::string_view prefix_chars) { + return !is_positional(name, prefix_chars); } /* @@ -832,20 +838,21 @@ private: * '-' decimal-literal * !'-' anything */ - static bool is_positional(std::string_view name) { - switch (lookahead(name)) { - case eof: + static bool is_positional(std::string_view name, + std::string_view prefix_chars) { + auto first = lookahead(name); + + if (first == eof) { return true; - case '-': { + } else if (prefix_chars.find(static_cast(first)) != + std::string::npos) { name.remove_prefix(1); if (name.empty()) { return true; } return is_decimal_literal(name); } - default: - return true; - } + return true; } /* @@ -921,6 +928,7 @@ private: bool m_is_required : true; bool m_is_repeatable : true; bool m_is_used : true; // True if the optional argument is used by user + std::string_view m_prefix_chars; // ArgumentParser has the prefix_chars }; class ArgumentParser { @@ -960,7 +968,7 @@ public: ArgumentParser(const ArgumentParser &other) : m_program_name(other.m_program_name), m_version(other.m_version), m_description(other.m_description), m_epilog(other.m_epilog), - m_is_parsed(other.m_is_parsed), + m_prefix_chars(other.m_prefix_chars), m_is_parsed(other.m_is_parsed), m_positional_arguments(other.m_positional_arguments), m_optional_arguments(other.m_optional_arguments), m_parser_path(other.m_parser_path), m_subparsers(other.m_subparsers) { @@ -991,8 +999,9 @@ public: // Call add_argument with variadic number of string arguments template Argument &add_argument(Targs... f_args) { using array_of_sv = std::array; - auto argument = m_optional_arguments.emplace( - std::cend(m_optional_arguments), array_of_sv{f_args...}); + auto argument = + m_optional_arguments.emplace(std::cend(m_optional_arguments), + m_prefix_chars, array_of_sv{f_args...}); if (!argument->m_is_optional) { m_positional_arguments.splice(std::cend(m_positional_arguments), @@ -1032,6 +1041,16 @@ public: return *this; } + ArgumentParser &set_prefix_chars(std::string prefix_chars) { + m_prefix_chars = std::move(prefix_chars); + return *this; + } + + ArgumentParser &set_assign_chars(std::string assign_chars) { + m_assign_chars = std::move(assign_chars); + return *this; + } + /* Call parse_args_internal - which does all the work * Then, validate the parsed arguments * This variant is used mainly for testing @@ -1126,16 +1145,19 @@ public: if (it != m_argument_map.end()) { return *(it->second); } - if (arg_name.front() != '-') { + if (!is_valid_prefix_char(arg_name.front())) { std::string name(arg_name); + const auto legal_prefix_char = get_any_valid_prefix_char(); + const auto prefix = std::string(1, legal_prefix_char); + // "-" + arg_name - name = "-" + name; + name = prefix + name; it = m_argument_map.find(name); if (it != m_argument_map.end()) { return *(it->second); } // "--" + arg_name - name = "-" + name; + name = prefix + name; it = m_argument_map.find(name); if (it != m_argument_map.end()) { return *(it->second); @@ -1226,6 +1248,12 @@ public: } private: + bool is_valid_prefix_char(char c) const { + return m_prefix_chars.find(c) != std::string::npos; + } + + char get_any_valid_prefix_char() const { return m_prefix_chars[0]; } + /* * Pre-process this argument list. Anything starting with "--", that * contains an =, where the prefix before the = has an entry in the @@ -1237,11 +1265,46 @@ private: for (const auto &arg : raw_arguments) { // Check that: // - We don't have an argument named exactly this - // - The argument starts with "--" - // - The argument contains a "=" - std::size_t eqpos = arg.find("="); + // - The argument starts with a prefix char, e.g., "--" + // - The argument contains an assign char, e.g., "=" + std::size_t eqpos = arg.find_first_of(m_assign_chars); + + static const auto argument_starts_with_prefix_chars = + [this](const std::string &a) { + if (a.size() > 0) { + + const auto legal_prefix = [this](char c) { + return m_prefix_chars.find(c) != std::string::npos; + }; + + // Windows-style + // if '/' is a legal prefix char + // then allow single '/' followed by argument name, followed by an + // assign char, e.g., ':' e.g., 'test.exe /A:Foo' + const auto windows_style = legal_prefix('/'); + + if (windows_style) { + if (legal_prefix(a[0])) { + return true; + } + } else { + // Slash '/' is not a legal prefix char + // For all other characters, only support long arguments + // i.e., the argument must start with 2 prefix chars, e.g, + // '--foo' e,g, './test --foo=Bar -DARG=yes' + if (a.size() > 1) { + if (legal_prefix(a[0]) && legal_prefix(a[1])) { + return true; + } + } + } + } + return false; + }; + if (m_argument_map.find(arg) == m_argument_map.end() && - arg.rfind("--", 0) == 0 && eqpos != std::string::npos) { + argument_starts_with_prefix_chars(arg) && + eqpos != std::string::npos) { // Get the name of the potential option, and check it exists std::string opt_name = arg.substr(0, eqpos); if (m_argument_map.find(opt_name) != m_argument_map.end()) { @@ -1269,7 +1332,7 @@ private: auto positional_argument_it = std::begin(m_positional_arguments); for (auto it = std::next(std::begin(arguments)); it != end;) { const auto ¤t_argument = *it; - if (Argument::is_positional(current_argument)) { + if (Argument::is_positional(current_argument, m_prefix_chars)) { if (positional_argument_it == std::end(m_positional_arguments)) { std::string_view maybe_command = current_argument; @@ -1302,8 +1365,9 @@ private: auto argument = arg_map_it->second; it = argument->consume(std::next(it), end, arg_map_it->first); } else if (const auto &compound_arg = current_argument; - compound_arg.size() > 1 && compound_arg[0] == '-' && - compound_arg[1] != '-') { + compound_arg.size() > 1 && + is_valid_prefix_char(compound_arg[0]) && + !is_valid_prefix_char(compound_arg[1])) { ++it; for (std::size_t j = 1; j < compound_arg.size(); j++) { auto hypothetical_arg = std::string{'-', compound_arg[j]}; @@ -1338,7 +1402,7 @@ private: auto positional_argument_it = std::begin(m_positional_arguments); for (auto it = std::next(std::begin(arguments)); it != end;) { const auto ¤t_argument = *it; - if (Argument::is_positional(current_argument)) { + if (Argument::is_positional(current_argument, m_prefix_chars)) { if (positional_argument_it == std::end(m_positional_arguments)) { std::string_view maybe_command = current_argument; @@ -1375,8 +1439,9 @@ private: auto argument = arg_map_it->second; it = argument->consume(std::next(it), end, arg_map_it->first); } else if (const auto &compound_arg = current_argument; - compound_arg.size() > 1 && compound_arg[0] == '-' && - compound_arg[1] != '-') { + compound_arg.size() > 1 && + is_valid_prefix_char(compound_arg[0]) && + !is_valid_prefix_char(compound_arg[1])) { ++it; for (std::size_t j = 1; j < compound_arg.size(); j++) { auto hypothetical_arg = std::string{'-', compound_arg[j]}; @@ -1429,6 +1494,8 @@ private: std::string m_version; std::string m_description; std::string m_epilog; + std::string m_prefix_chars{"-"}; + std::string m_assign_chars{"="}; bool m_is_parsed = false; std::list m_positional_arguments; std::list m_optional_arguments; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b61040f..b228feb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -49,6 +49,7 @@ file(GLOB ARGPARSE_TEST_SOURCES test_subparsers.cpp test_parse_known_args.cpp test_equals_form.cpp + test_prefix_chars.cpp ) set_source_files_properties(main.cpp PROPERTIES diff --git a/test/test_prefix_chars.cpp b/test/test_prefix_chars.cpp new file mode 100644 index 0000000..e62b1d9 --- /dev/null +++ b/test/test_prefix_chars.cpp @@ -0,0 +1,41 @@ +#include +#include +#include + +using doctest::test_suite; + +TEST_CASE("Parse with custom prefix chars" * test_suite("prefix_chars")) { + argparse::ArgumentParser program("test"); + program.set_prefix_chars("-+"); + program.add_argument("+f"); + program.add_argument("++bar"); + program.parse_args({"test", "+f", "X", "++bar", "Y"}); + REQUIRE(program.get("+f") == "X"); + REQUIRE(program.get("++bar") == "Y"); +} + +TEST_CASE("Parse with custom Windows-style prefix chars" * + test_suite("prefix_chars")) { + argparse::ArgumentParser program("dir"); + program.set_prefix_chars("/"); + program.add_argument("/A").nargs(1); + program.add_argument("/B").default_value(false).implicit_value(true); + program.add_argument("/C").default_value(false).implicit_value(true); + program.parse_args({"dir", "/A", "D", "/B", "/C"}); + REQUIRE(program.get("/A") == "D"); + REQUIRE(program.get("/B") == true); +} + +TEST_CASE("Parse with custom Windows-style prefix chars and assign chars" * + test_suite("prefix_chars")) { + argparse::ArgumentParser program("dir"); + program.set_prefix_chars("/"); + program.set_assign_chars(":="); + program.add_argument("/A").nargs(1); + program.add_argument("/B").nargs(1); + program.add_argument("/C").default_value(false).implicit_value(true); + program.parse_args({"dir", "/A:D", "/B=Boo", "/C"}); + REQUIRE(program.get("/A") == "D"); + REQUIRE(program.get("/B") == "Boo"); + REQUIRE(program.get("/C") == true); +} \ No newline at end of file From 632ca2fcf8b19ec371403b7536ed139fb0f48d61 Mon Sep 17 00:00:00 2001 From: Pranav Srinivas Kumar Date: Wed, 21 Sep 2022 09:53:30 -0700 Subject: [PATCH 2/3] Added prefix_chars and assign_chars to README #67 --- README.md | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 82f47ed..8589b3b 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ * [Subcommands](#subcommands) * [Parse Known Args](#parse-known-args) * [Custom Prefix Characters](#custom-prefix-characters) + * [Custom Assignment Characters](#custom-assignment-characters) * [Further Examples](#further-examples) * [Construct a JSON object from a filename argument](#construct-a-json-object-from-a-filename-argument) * [Positional Arguments with Compound Toggle Arguments](#positional-arguments-with-compound-toggle-arguments) @@ -826,7 +827,9 @@ int main(int argc, char *argv[]) { ### Custom Prefix Characters -Most command-line options will use `-` as the prefix, e.g. `-f/--foo`. Parsers that need to support different or additional prefix characters, e.g. for options like `+f` or `/foo`, may specify them using the `set_prefix_chars()`: +Most command-line options will use `-` as the prefix, e.g. `-f/--foo`. Parsers that need to support different or additional prefix characters, e.g. for options like `+f` or `/foo`, may specify them using the `set_prefix_chars()`. + +The default prefix character is `-`. ```cpp #include @@ -870,6 +873,49 @@ foo@bar:/home/dev/$ ./main +f 5 --bar 3.14f /foo "Hello" /foo : Hello ``` +### Custom Assignment Characters + +In addition to prefix characters, custom 'assign' characters can be set. This setting is used to allow invocations like `./test --foo=Foo /B:Bar`. + +The default assign character is `=`. + +```cpp +#include +#include + +int main(int argc, char *argv[]) { + argparse::ArgumentParser program("test"); + program.set_prefix_chars("-+/"); + program.set_assign_chars("=:"); + + program.add_argument("--foo"); + program.add_argument("/B"); + + try { + program.parse_args(argc, argv); + } + catch (const std::runtime_error& err) { + std::cerr << err.what() << std::endl; + std::cerr << program; + std::exit(1); + } + + if (program.is_used("--foo")) { + std::cout << "--foo : " << program.get("--foo") << "\n"; + } + + if (program.is_used("/B")) { + std::cout << "/B : " << program.get("/B") << "\n"; + } +} +``` + +```console +foo@bar:/home/dev/$ ./main --foo=Foo /B:Bar +--foo : Foo +/B : Bar +``` + ## Further Examples ### Construct a JSON object from a filename argument From d56515f6dffc9f3c73950d071d6713ae0b950fa3 Mon Sep 17 00:00:00 2001 From: Pranav Srinivas Kumar Date: Wed, 21 Sep 2022 10:58:25 -0700 Subject: [PATCH 3/3] Removed static from lambda function, updated copy constructor for m_assign_chars --- include/argparse/argparse.hpp | 82 +++++++++++++++++------------------ 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index b0a1076..ba27b43 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -845,7 +845,7 @@ private: if (first == eof) { return true; } else if (prefix_chars.find(static_cast(first)) != - std::string::npos) { + std::string_view::npos) { name.remove_prefix(1); if (name.empty()) { return true; @@ -968,7 +968,8 @@ public: ArgumentParser(const ArgumentParser &other) : m_program_name(other.m_program_name), m_version(other.m_version), m_description(other.m_description), m_epilog(other.m_epilog), - m_prefix_chars(other.m_prefix_chars), m_is_parsed(other.m_is_parsed), + m_prefix_chars(other.m_prefix_chars), + m_assign_chars(other.m_assign_chars), m_is_parsed(other.m_is_parsed), m_positional_arguments(other.m_positional_arguments), m_optional_arguments(other.m_optional_arguments), m_parser_path(other.m_parser_path), m_subparsers(other.m_subparsers) { @@ -1260,57 +1261,56 @@ private: * options table, should be split. */ std::vector - preprocess_arguments(const std::vector &raw_arguments) { - std::vector arguments; + preprocess_arguments(const std::vector &raw_arguments) const { + std::vector arguments{}; for (const auto &arg : raw_arguments) { + + const auto argument_starts_with_prefix_chars = + [this](const std::string &a) -> bool { + if (!a.empty()) { + + const auto legal_prefix = [this](char c) -> bool { + return m_prefix_chars.find(c) != std::string::npos; + }; + + // Windows-style + // if '/' is a legal prefix char + // then allow single '/' followed by argument name, followed by an + // assign char, e.g., ':' e.g., 'test.exe /A:Foo' + const auto windows_style = legal_prefix('/'); + + if (windows_style) { + if (legal_prefix(a[0])) { + return true; + } + } else { + // Slash '/' is not a legal prefix char + // For all other characters, only support long arguments + // i.e., the argument must start with 2 prefix chars, e.g, + // '--foo' e,g, './test --foo=Bar -DARG=yes' + if (a.size() > 1) { + return (legal_prefix(a[0]) && legal_prefix(a[1])); + } + } + } + return false; + }; + // Check that: // - We don't have an argument named exactly this // - The argument starts with a prefix char, e.g., "--" // - The argument contains an assign char, e.g., "=" - std::size_t eqpos = arg.find_first_of(m_assign_chars); - - static const auto argument_starts_with_prefix_chars = - [this](const std::string &a) { - if (a.size() > 0) { - - const auto legal_prefix = [this](char c) { - return m_prefix_chars.find(c) != std::string::npos; - }; - - // Windows-style - // if '/' is a legal prefix char - // then allow single '/' followed by argument name, followed by an - // assign char, e.g., ':' e.g., 'test.exe /A:Foo' - const auto windows_style = legal_prefix('/'); - - if (windows_style) { - if (legal_prefix(a[0])) { - return true; - } - } else { - // Slash '/' is not a legal prefix char - // For all other characters, only support long arguments - // i.e., the argument must start with 2 prefix chars, e.g, - // '--foo' e,g, './test --foo=Bar -DARG=yes' - if (a.size() > 1) { - if (legal_prefix(a[0]) && legal_prefix(a[1])) { - return true; - } - } - } - } - return false; - }; + auto assign_char_pos = arg.find_first_of(m_assign_chars); if (m_argument_map.find(arg) == m_argument_map.end() && argument_starts_with_prefix_chars(arg) && - eqpos != std::string::npos) { + assign_char_pos != std::string::npos) { // Get the name of the potential option, and check it exists - std::string opt_name = arg.substr(0, eqpos); + std::string opt_name = arg.substr(0, assign_char_pos); if (m_argument_map.find(opt_name) != m_argument_map.end()) { // This is the name of an option! Split it into two parts arguments.push_back(std::move(opt_name)); - arguments.push_back(arg.substr(eqpos + 1)); + arguments.push_back(arg.substr(assign_char_pos + 1)); continue; } }