mirror of
https://github.com/KeqingMoe/argparse.git
synced 2025-07-03 14:44:40 +00:00
Merge pull request #205 from p-ranav/feature/prefix_chars
Closes #67: option-value syntax support
This commit is contained in:
commit
176b7e7961
93
README.md
93
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 <argparse/argparse.hpp>
|
||||
#include <cassert>
|
||||
|
||||
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 <argparse/argparse.hpp>
|
||||
#include <cassert>
|
||||
|
||||
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
|
||||
|
@ -355,10 +355,12 @@ class Argument {
|
||||
-> std::ostream &;
|
||||
|
||||
template <std::size_t N, std::size_t... I>
|
||||
explicit Argument(std::array<std::string_view, N> &&a,
|
||||
explicit Argument(std::string_view prefix_chars,
|
||||
std::array<std::string_view, N> &&a,
|
||||
std::index_sequence<I...> /*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 <std::size_t N>
|
||||
explicit Argument(std::array<std::string_view, N> &&a)
|
||||
: Argument(std::move(a), std::make_index_sequence<N>{}) {}
|
||||
explicit Argument(std::string_view prefix_chars,
|
||||
std::array<std::string_view, N> &&a)
|
||||
: Argument(prefix_chars, std::move(a), std::make_index_sequence<N>{}) {}
|
||||
|
||||
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::size_t>(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<char>(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 <typename... Targs> Argument &add_argument(Targs... f_args) {
|
||||
using array_of_sv = std::array<std::string_view, sizeof...(Targs)>;
|
||||
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<std::string>
|
||||
preprocess_arguments(const std::vector<std::string> &raw_arguments) {
|
||||
std::vector<std::string> arguments;
|
||||
preprocess_arguments(const std::vector<std::string> &raw_arguments) const {
|
||||
std::vector<std::string> 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<Argument> m_positional_arguments;
|
||||
std::list<Argument> m_optional_arguments;
|
||||
|
@ -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
|
||||
|
41
test/test_prefix_chars.cpp
Normal file
41
test/test_prefix_chars.cpp
Normal file
@ -0,0 +1,41 @@
|
||||
#include <argparse/argparse.hpp>
|
||||
#include <cmath>
|
||||
#include <doctest.hpp>
|
||||
|
||||
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<bool>("/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<bool>("/C") == true);
|
||||
}
|
Loading…
Reference in New Issue
Block a user