diff --git a/README.md b/README.md index 7093f0b..8589b3b 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ * [Parent Parsers](#parent-parsers) * [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) @@ -823,6 +825,97 @@ 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()`. + +The default prefix character is `-`. + +```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 +``` + +### 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 diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index c0e13dd..ba27b43 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_view::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,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_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) { @@ -991,8 +1000,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 +1042,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 +1146,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,28 +1249,68 @@ 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 * 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 "--" - // - 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., "=" + auto assign_char_pos = arg.find_first_of(m_assign_chars); + if (m_argument_map.find(arg) == m_argument_map.end() && - arg.rfind("--", 0) == 0 && eqpos != std::string::npos) { + argument_starts_with_prefix_chars(arg) && + 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; } } @@ -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