diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 548aa38..18bfd80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,13 @@ jobs: os: [ ubuntu-22.04 ] cxx: [ g++-10, g++-13, clang++-13, clang++-15 ] cxxstd: [ c++14, c++17, c++20 ] + exclude: # exclusions to work around https://github.com/actions/runner-images/issues/8659 + - os: "ubuntu-22.04" + cxx: clang++-15 + cxxstd: c++20 + - os: "ubuntu-22.04" + cxx: clang++-13 + cxxstd: c++20 include: - os: "macos-latest" cxx: "clang++" diff --git a/Makefile b/Makefile index 772aaee..4b3c777 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ top:=$(dir $(realpath $(lastword $(MAKEFILE_LIST)))) -examples:=ex1-parse ex1-run ex2-parse ex2-run ex3-parse ex3-run ex4-run ex5-run +examples:=ex1-parse ex1-run ex2-parse ex2-run ex3-parse ex3-run ex4-run ex5-run ex6-run ex7-run all:: unit $(examples) test-src:=unit.cc test_sink.cc test_maybe.cc test_option.cc test_state.cc test_parse.cc test_parsers.cc test_saved_options.cc test_run.cc test_version.cc @@ -58,6 +58,12 @@ ex4-run: ex4-run.o ex5-run: ex5-run.o $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) +ex6-run: ex6-run.o + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) + +ex7-run: ex7-run.o + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) + clean: rm -f $(all-obj) diff --git a/README.md b/README.md index e87818b..2e5906b 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Non-features: This is due to laziness. But it does try to at least not break UTF-8. * _Does not automatically generate help/usage text._ What constitutes good help output is too specific to any given program. -* _Does not support optional or multiple arguments to an option._ +* _Does not support multiple arguments to an option._ This is mainly due to problems of ambiguous parsing, though in a pinch this can be set up through the use of modal option parsing (see _Filters and Modals_ below). @@ -186,8 +186,8 @@ that contains a value, or by any other value that is not `nothing`. `something` is a pre-defined non-empty value of type `maybe`. `maybe` values support basic monadic-like functionality via `operator<<`. -* If `x` is an lvalue and `m` is of type `maybe`, then - `x << m` has type `maybe` (`V` is the type of `x=*m`) and assigns `m.value()` to `x` +* If `x` is an lvalue and `m` is of type `maybe`, then + `x << m` has type `maybe` where `V` is the type of `x=*m` and assigns `m.value()` to `x` if `m` has a value. In the case that `U` is `void`, then the value of `m` is taken to be `true`. * If `f` is a function or function object with signature `V f(U)`, and `m` is of type `maybe`, then @@ -223,7 +223,7 @@ An alternative prefix to "Usage: " can be supplied optionally. A parser is a function or functional object with signature `maybe (const char*)` for some type `X`. They are used to try to convert a C-string argument into a value. -If no explicit parser is given to the`parse` function or to an `option` specification, +If no explicit parser is given to the `parse` function or to an `option` specification, the default parser `default_parser` is used, which will use `std::istream::operator>>` to read the supplied argument. @@ -455,6 +455,8 @@ Option behaviour can be modified by supplying `enum option_flag` values: * `mandatory` — Throw an exception if this option does not appear in the command line arguments. * `exit` — On successful parsing of this option, stop any further option processing and return `nothing` from `run()`. * `stop` — On successful parsing of this option, stop any further option processing but return saved options as normal from `run()`. +* `lax` — If the argument parsing is unsuccessful or the sink otherwise returns false, disregard this option + instead of throwing a `missing_argument` or `option_parse_error` exception. These enum values are all powers of two and can be combined via bitwise or `|`. @@ -513,6 +515,13 @@ Some example specifications: { to::set(b), "-b"_compact, to::flag }, { to::set(c), "-c"_compact, to::flag } }; + + // Implementing an option with optional argument with to::lax. + // (opt_u must precede opt_u_flag in the sequence of options passed to to::run). + maybe u; + int default_u = 3; + to::option opt_u = { u, "-u", to::lax }; + to::option opt_u_flag = { to::set(u, default_u), "-u", to::flag }; ``` #### Saved options @@ -572,5 +581,49 @@ Like the `to::parse` functions, the `run()` function can throw `missing_argument marked with `mandatory` is not found during command line argument parsing. Note that the arguments in `argv` are checked from the beginning; when calling `run` from within, -e.g the main function `int main(int argc, char** argv)`, one should pass `argv+1` to `run` +e.g. the main function `int main(int argc, char** argv)`, one should pass `argv+1` to `run` so as to avoid including the program name in `argv[0]`. + +### How do I …? + +#### How do I make an option that accepts multiple arguments? + +Tinyopt does not support this directly but the modal option facility can provide this functionality. +The following example uses an option `-n` to collect up to five integer values into a vector by switching to a new +mode and using key-less options to match those integers. (Compare with Example 7.) + +``` + std::vector> nss; + auto new_ns = [&nss] { nss.push_back({}); }; + auto push_ns = [&nss](int n) { nss.back().push_back(n); }; + + auto gt0 = [](int m) { return m>0; }; + auto decr = [](int m) { return m-1; }; + + to::options opts[] = { + { to::action(new_ns), to::flag, "-n", to::then(5) }, + { to::action(push_ns), to::flag, to::when(gt0), to::then(decr); }, + }; +``` + +Here, the `-n` flag pushes a new vector onto `nss` and changes the mode to 5, while the keyless option pushes +integers onto the vector if the mode is greater than zero and decrements the mode. + +#### How do I make an option with an optional argument? + +If an option is marked as `lax`, a failure to parse the argument does not throw an exception and `to::run` will then try to match +other options. This can be used to implement a flag with optional argument: + +``` + maybe value; + int default_value = 3; + + to::options opts[] = { + { value, "-n", to::lax }, + { to::set(value, default_value), "-n", to::flag } + }; +``` + +If `argv` has a sequence such as `-n foo`, the first option with key `"-n"` will fail to parse the argument as an integer and +to::run will then try the next `"-n"` option, which is a flag. `foo` remains in `argv` for further processing. (Compare with Example +6.) diff --git a/ex/ex1-parse.cc b/ex/ex1-parse.cc index 9ed713c..c244492 100644 --- a/ex/ex1-parse.cc +++ b/ex/ex1-parse.cc @@ -5,9 +5,11 @@ const char* usage_str = "[OPTION]...\n" "\n" - " -n, --number=N Specify N\n" - " -f, --function=FUNC Perform FUNC, which is one of: one, two\n" - " -h, --help Display usage information and exit\n"; + " -n, --number=N specify number of times to perform function\n" + " -f, --function=FUNC specify function, which is one of: one, two;\n" + " this option is mandatory\n" + "\n" + " -h, --help display usage information and exit\n"; int main(int, char** argv) { try { diff --git a/ex/ex1-run.cc b/ex/ex1-run.cc index 1a794d9..af0db92 100644 --- a/ex/ex1-run.cc +++ b/ex/ex1-run.cc @@ -5,9 +5,11 @@ const char* usage_str = "[OPTION]...\n" "\n" - " -n, --number=N Specify N\n" - " -f, --function=FUNC Perform FUNC, which is one of: one, two\n" - " -h, --help Display usage information and exit\n"; + " -n, --number=N specify number of times to perform function\n" + " -f, --function=FUNC specify function, which is one of: one, two;\n" + " this option is mandatory\n" + "\n" + " -h, --help display usage information and exit\n"; int main(int argc, char** argv) { try { @@ -27,7 +29,7 @@ int main(int argc, char** argv) { if (!to::run(opts, argc, argv+1)) return 0; - if (argv[1]) throw to::option_error("unrecogonized argument", argv[1]); + if (argv[1]) throw to::option_error("unrecognized argument", argv[1]); if (n<1) throw to::option_error("N must be at least 1"); // Do things with arguments: diff --git a/ex/ex2-parse.cc b/ex/ex2-parse.cc index 12bc98a..86b6597 100644 --- a/ex/ex2-parse.cc +++ b/ex/ex2-parse.cc @@ -7,8 +7,8 @@ const char* usage_str = "[OPTION]...\n" "\n" - " --sum=N1,..,Nk Sum the integers N1 through Nk.\n" - " -h, --help Display usage information and exit\n"; + " --sum=N1,..,Nk sum the integers N1 through Nk\n" + " -h, --help display usage information and exit\n"; int main(int, char** argv) { try { diff --git a/ex/ex2-run.cc b/ex/ex2-run.cc index 7c23375..6931944 100644 --- a/ex/ex2-run.cc +++ b/ex/ex2-run.cc @@ -7,8 +7,8 @@ const char* usage_str = "[OPTION]...\n" "\n" - " --sum=N1,..,Nk Sum the integers N1 through Nk.\n" - " -h, --help Display usage information and exit\n"; + " --sum=N1,..,Nk sum the integers N1 through Nk\n" + " -h, --help display usage information and exit\n"; int main(int argc, char** argv) { try { diff --git a/ex/ex3-parse.cc b/ex/ex3-parse.cc index 9c3f195..e680b91 100644 --- a/ex/ex3-parse.cc +++ b/ex/ex3-parse.cc @@ -7,11 +7,12 @@ const char* usage_str = "[OPTION]... [ARGUMENT]...\n" "\n" - " -a, --apple Print 'apple' but otherwise ignore.\n" - " -- Stop further argument processing.\n" - " -h, --help Display usage information and exit.\n" + " -a, --apple print 'apple'\n" "\n" - "Throw away --apple options and report remaining arguments.\n"; + " -- stop further argument processing\n" + " -h, --help display usage information and exit\n" + "\n" + "Disregarding --apple options, report remaining arguments.\n"; int main(int, char** argv) { try { diff --git a/ex/ex3-run.cc b/ex/ex3-run.cc index f05b593..7de0d06 100644 --- a/ex/ex3-run.cc +++ b/ex/ex3-run.cc @@ -7,11 +7,12 @@ const char* usage_str = "[OPTION]... [ARGUMENT]...\n" "\n" - " -a, --apple Print 'apple' but otherwise ignore.\n" - " -- Stop further argument processing.\n" - " -h, --help Display usage information and exit.\n" + " -a, --apple print 'apple'\n" "\n" - "Throw away --apple options and report remaining arguments.\n"; + " -- stop further argument processing\n" + " -h, --help display usage information and exit\n" + "\n" + "Disregarding --apple options, report remaining arguments.\n"; int main(int argc, char** argv) { try { diff --git a/ex/ex6-run.cc b/ex/ex6-run.cc new file mode 100644 index 0000000..72d0473 --- /dev/null +++ b/ex/ex6-run.cc @@ -0,0 +1,48 @@ +#include +#include + +#include + +const char* usage_str = + "[OPTIONS]...\n" + "\n" + " -n fish | cake print a message indicating a keyword argument\n" + " -n INT print a message indicating aninteger argument\n" + " -n print a message indicating no argument\n" + " -h, --help display usage information and exit\n"; + +void print_kw(const char* kw) { + std::cout << "keyword argument: " << kw << "\n"; +} + +void print_int(int n) { + std::cout << "integer argument: " << n << "\n"; +} + +void print_flag() { + std::cout << "no argument\n"; +} + +int main(int argc, char** argv) { + try { + std::pair kw_tbl[] = { + { "fish", "FISH" }, { "cake", "CAKE" } + }; + + auto help = [argv0 = argv[0]] { to::usage(argv0, usage_str); }; + + to::option opts[] = { + { to::action(help), "-h", "--help" }, + { to::action(print_kw, to::keywords(kw_tbl)), "-n", to::lax }, + { to::action(print_int, to::default_parser{}), "-n", to::lax }, + { to::action(print_flag), "-n", to::flag }, + }; + + to::run(opts, argc, argv+1); + if (argv[1]) throw to::option_error("unrecognized argument", argv[1]); + } + catch (to::option_error& e) { + to::usage_error(argv[0], usage_str, e.what()); + return 1; + } +} diff --git a/ex/ex7-run.cc b/ex/ex7-run.cc new file mode 100644 index 0000000..43a38f1 --- /dev/null +++ b/ex/ex7-run.cc @@ -0,0 +1,47 @@ +#include +#include +#include + +#include + +const char* usage_str = + "[OPTION] ...\n" + "\n" + " -n [ INT [ INT [ INT ] ] ] collect a vector of up to 3 integers\n" + " -h, --help display usage information and exit\n" + "\n" + "Collect and display vectors of up to 3 integers as multiple arguments to the -n option.\n"; + +int main(int argc, char** argv) { + try { + auto help = [argv0 = argv[0]] { to::usage(argv0, usage_str); }; + + std::vector> nss; + + auto new_ns = [&]() { nss.push_back({}); return true; }; + auto push_ns = [&](int n) { nss.back().push_back(n); return true; }; + + auto gt0 = [](int m) { return m>0; }; + auto decrement = [](int m) { return m-1; }; + + to::option opts[] = { + { to::action(help), "-h", "--help", to::flag, to::exit }, + { to::action(new_ns), to::then(3), "-n", to::flag }, + { to::action(push_ns), to::when(gt0), to::then(decrement)} + }; + + if (!to::run(opts, argc, argv+1)) return 0; + if (argv[1]) throw to::option_error("unrecognized argument", argv[1]); + + for (auto& ns: nss) { + std::cout << "{ "; + std::ostream_iterator os(std::cout, " "); + for (int n: ns) *os = n; + std::cout << "}\n"; + } + } + catch (to::option_error& e) { + to::usage_error(argv[0], usage_str, e.what()); + return 1; + } +} diff --git a/include/tinyopt/tinyopt.h b/include/tinyopt/tinyopt.h index 97f6e42..720699e 100644 --- a/include/tinyopt/tinyopt.h +++ b/include/tinyopt/tinyopt.h @@ -453,6 +453,8 @@ struct state { // Shift arguments left in-place. void shift(unsigned n = 1) { + if (!n) return; + char** skip = argv; while (*skip && n) ++skip, --n; @@ -468,33 +470,36 @@ struct state { if (*argv) ++argv; } + struct match_result { + const char* argument = nullptr; + unsigned shift = 0; + unsigned offset = 0; + }; + + void consume(const match_result& mr) { + shift(mr.shift); + optoff += mr.offset; + } + // Match an option given by the key which takes an argument. // If successful, consume option and argument and return pointer to // argument string, else return nothing. - maybe match_option(const key& k) { - const char* p = nullptr; - + maybe match_option(const key& k) { if (k.style==key::compact) { - if ((p = match_compact_key(k.label.c_str()))) { - if (!*p) { - p = argv[1]; - shift(2); - } - else shift(); - return p; + if (auto m = match_compact_key(k.label.c_str())) { + if ((*argv)[optoff+*m]) + return match_result{*argv+optoff+*m, 1, 0}; + else + return match_result{argv[1], 2, 0}; } } else if (!optoff && k.label==*argv) { - p = argv[1]; - shift(2); - return p; + return match_result{argv[1], 2, 0}; } else if (!optoff && k.style==key::longfmt) { auto keylen = k.label.length(); if (!std::strncmp(*argv, k.label.c_str(), keylen) && (*argv)[keylen]=='=') { - p = &(*argv)[keylen+1]; - shift(); - return p; + return match_result{*argv+keylen+1, 1, 0}; } } @@ -502,37 +507,40 @@ struct state { } // Match a flag given by the key. - // If successful, consume flag and return true, else return false. - bool match_flag(const key& k) { + // If successful, consume match_result with nullptr for option argvument. + maybe match_flag(const key& k) { if (k.style==key::compact) { - if (auto p = match_compact_key(k.label.c_str())) { - if (!*p) shift(); - return true; + if (auto m = match_compact_key(k.label.c_str())) { + if ((*argv)[optoff+*m]) + return match_result{nullptr, 0, *m}; + else + return match_result{nullptr, 1, 0}; } } else if (!optoff && k.label==*argv) { - shift(); - return true; + return match_result{nullptr, 1, 0}; } - return false; + return nothing; } // Compact-style keys can be combined in one argument; combined keys // with a common prefix only need to supply the prefix once at the // beginning of the argument. - const char* match_compact_key(const char* k) { + // + // On success returns number of characters constituting the compact key. + maybe match_compact_key(const char* k) { unsigned keylen = std::strlen(k); unsigned prefix_max = std::min(keylen-1, optoff); for (std::size_t l = 0; l<=prefix_max; ++l) { if (l && strncmp(*argv, k, l)) break; if (strncmp(*argv+optoff, k+l, keylen-l)) continue; - optoff += keylen-l; - return *argv+optoff; + + return keylen-l; } - return nullptr; + return nothing; } }; @@ -601,7 +609,6 @@ struct sink { bool operator()(const char* param) const { return op(param); } std::function op; - }; // Convenience functions for construction of sink actions @@ -743,6 +750,7 @@ enum option_flag { mandatory = 8, // Option must be present in argument list. exit = 16, // Option stops further argument processing, return `nothing` from run(). stop = 32, // Option stops further argument processing, return saved options. + lax = 64, // Option does not throw an error if argument fails to parse or is missing. }; struct option { @@ -757,6 +765,7 @@ struct option { bool is_mandatory = false; bool is_exit = false; bool is_stop = false; + bool is_lax = false; template option(sink s, Rest&&... rest): s(std::move(s)) { @@ -782,9 +791,11 @@ struct option { for (auto& f: modals) mode = f(mode); } - void run(const std::string& label, const char* arg) const { - if (!is_flag && !arg) throw missing_argument(label); - if (!s(arg)) throw option_parse_error(label); + // Returns false (if lax) or throws on argument error. + bool run(const std::string& label, const char* arg) const { + if (!is_flag && !arg) return is_lax? false: throw missing_argument(label); + if (!s(arg)) return is_lax? false: throw option_parse_error(label); + return true; } std::string longest_label() const { @@ -806,6 +817,7 @@ struct option { is_mandatory |= f & mandatory; is_exit |= f & exit; is_stop |= f & stop; + is_lax |= f & lax; init_(std::forward(rest)...); } @@ -826,7 +838,6 @@ struct option { keys.push_back(std::move(k)); init_(std::forward(rest)...); } - }; // Saved options @@ -947,39 +958,33 @@ namespace impl { counted_option(const option& o): option(o) {} - // On successful match, return pointers to matched key and value. - // For flags, use nullptr for value; for empty key sets, use + // On successful match, return pointers to matched key and argumnent. + // For flags, use nullptr for argumnent; for empty key sets, use // nullptr for key. - maybe> match(state& st) { - if (is_flag) { - for (auto& k: keys) { - if (st.match_flag(k)) return set(k.label, nullptr); + typedef maybe> maybe_keyarg; + maybe_keyarg match(state& st) { + bool empty_keyset = keys.empty(); + + auto try_run = [&](const std::string& label, const state::match_result& mr) -> maybe_keyarg { + if (run(label, mr.argument)) { + st.consume(mr); + ++count; + return std::make_pair(empty_keyset? nullptr: label.c_str(), mr.argument); } return nothing; + }; + + if (is_flag) { + for (auto& k: keys) if (auto m = st.match_flag(k)) return try_run(k.label, *m); } - else if (!keys.empty()) { - for (auto& k: keys) { - if (auto param = st.match_option(k)) return set(k.label, *param); - } - return nothing; + else if (!empty_keyset) { + for (auto& k: keys) if (auto m = st.match_option(k)) return try_run(k.label, *m); } else { - const char* param = *st.argv; - st.shift(); - return set("", param); + return try_run("", state::match_result{*st.argv, 1, 0}); } - } - - std::pair set(const char* arg) { - run("", arg); - ++count; - return {nullptr, arg}; - } - std::pair set(const std::string& label, const char* arg) { - run(label, arg); - ++count; - return {label.c_str(), arg}; + return nothing; } }; } // namespace impl diff --git a/test/googletest b/test/googletest index ba96d0b..5df0241 160000 --- a/test/googletest +++ b/test/googletest @@ -1 +1 @@ -Subproject commit ba96d0b1161f540656efdaed035b3c062b60e006 +Subproject commit 5df0241ea4880e5a846775d3efc8b873f7b36c31 diff --git a/test/test_option.cc b/test/test_option.cc index cd4fa47..e16cb60 100644 --- a/test/test_option.cc +++ b/test/test_option.cc @@ -47,8 +47,8 @@ TEST(key, literal) { TEST(option, ctor) { using namespace to::literals; - int a, b, c, d, e, f; - std::string g; + int a, b, c, d, e, f, g; + std::string h; to::option opts[] = { {a, to::ephemeral, to::single, "-a", "--arg"}, @@ -57,28 +57,33 @@ TEST(option, ctor) { {to::action([&d](int) {++d;}), "-d"_compact}, {e, to::flag, to::when(3), to::then(4), "-a"}, {f, to::then(17), to::then([](int k) { return k+1; }), "-a"}, - {g}, + {g, "-g", to::lax, "--gee"}, + {h}, }; EXPECT_FALSE(opts[0].is_flag); EXPECT_TRUE(opts[0].is_ephemeral); EXPECT_TRUE(opts[0].is_single); EXPECT_FALSE(opts[0].is_mandatory); + EXPECT_FALSE(opts[0].is_lax); EXPECT_FALSE(opts[1].is_flag); EXPECT_FALSE(opts[1].is_ephemeral); EXPECT_FALSE(opts[1].is_single); EXPECT_TRUE(opts[1].is_mandatory); + EXPECT_FALSE(opts[1].is_lax); EXPECT_TRUE(opts[2].is_flag); EXPECT_FALSE(opts[2].is_ephemeral); EXPECT_FALSE(opts[2].is_single); EXPECT_FALSE(opts[2].is_mandatory); + EXPECT_FALSE(opts[2].is_lax); EXPECT_FALSE(opts[3].is_flag); EXPECT_FALSE(opts[3].is_ephemeral); EXPECT_FALSE(opts[3].is_single); EXPECT_FALSE(opts[3].is_mandatory); + EXPECT_FALSE(opts[3].is_lax); EXPECT_TRUE(opts[4].is_flag); EXPECT_EQ(1u, opts[4].filters.size()); @@ -87,6 +92,8 @@ TEST(option, ctor) { EXPECT_EQ(0u, opts[5].filters.size()); EXPECT_EQ(2u, opts[5].modals.size()); + EXPECT_TRUE(opts[6].is_lax); + using svec = std::vector; auto key_labels = [](const to::option& o) { svec labels; @@ -96,7 +103,8 @@ TEST(option, ctor) { EXPECT_EQ((svec{"-a", "--arg"}), key_labels(opts[0])); EXPECT_EQ((svec{"-d"}), key_labels(opts[3])); - EXPECT_EQ((svec{}), key_labels(opts[6])); + EXPECT_EQ((svec{"-g", "--gee"}), key_labels(opts[6])); + EXPECT_EQ((svec{}), key_labels(opts[7])); } TEST(option, longest_label) { @@ -150,25 +158,48 @@ TEST(option, run) { using namespace to::literals; int a, c, d; std::string e; + bool rv = false; to::option opt_a{a, to::single, "-a", "--arg"}; - ASSERT_NO_THROW(opt_a.run("-a", "3")); + ASSERT_NO_THROW((rv = opt_a.run("-a", "3"))); + EXPECT_TRUE(rv); EXPECT_EQ(a, 3); - ASSERT_THROW(opt_a.run("-a", "fish"), to::option_parse_error); + ASSERT_THROW((rv = opt_a.run("-a", "fish")), to::option_parse_error); + EXPECT_TRUE(rv); + + to::option opt_a_lax{a, to::single, "-a", "--arg", to::lax}; + rv = true; + a = 5; + ASSERT_NO_THROW((rv = opt_a_lax.run("-a", "fish"))); + EXPECT_FALSE(rv); c = 1; to::option opt_c{to::increment(c), to::flag, "-c", "--cat"}; - ASSERT_NO_THROW(opt_c.run("-c", nullptr)); + rv = false; + ASSERT_NO_THROW((rv = opt_c.run("-c", nullptr))); + EXPECT_EQ(c, 2); + EXPECT_TRUE(rv); + + c = 1; + to::option opt_c_lax{to::increment(c), to::flag, "-c", "--cat"}; + rv = false; + ASSERT_NO_THROW((rv = opt_c_lax.run("-c", nullptr))); EXPECT_EQ(c, 2); + EXPECT_TRUE(rv); d = 3; to::option opt_d{to::action([&d](int) {++d;}), "-d"_compact}; - ASSERT_NO_THROW(opt_d.run("-d", "7")); + rv = false; + ASSERT_NO_THROW((rv = opt_d.run("-d", "7"))); + EXPECT_TRUE(rv); EXPECT_EQ(d, 4); - ASSERT_THROW(opt_d.run("-d", "fish"), to::option_parse_error); + ASSERT_THROW((rv = opt_d.run("-d", "fish")), to::option_parse_error); + EXPECT_TRUE(rv); to::option opt_e{e}; - ASSERT_NO_THROW(opt_e.run("", "bauble")); + rv = false; + ASSERT_NO_THROW((rv = opt_e.run("", "bauble"))); + EXPECT_TRUE(rv); EXPECT_EQ("bauble", e); } diff --git a/test/test_state.cc b/test/test_state.cc index d1ccbb5..94238c5 100644 --- a/test/test_state.cc +++ b/test/test_state.cc @@ -14,9 +14,13 @@ TEST(state, shift) { mockargs M(argstr); std::vector v0 = M.args; - to::state s(M.argc, M.argv); + s.shift(0); + EXPECT_EQ(7, M.argc); + EXPECT_EQ(0, M.argv[M.argc]); + EXPECT_EQ(v0[0], M.argv[0]); + s.shift(); EXPECT_EQ(6, M.argc); EXPECT_EQ(0, M.argv[M.argc]); @@ -37,6 +41,42 @@ TEST(state, shift) { EXPECT_EQ(v0[4], s.argv[0]); } +TEST(state, consume) { + const char* argstr = "zero\0one\0two\0three\0four\0five\0six\0"; + mockargs M(argstr); + + std::vector v0 = M.args; + to::state s(M.argc, M.argv); + + typedef to::state::match_result MR; + + + s.consume(MR{nullptr, 0, 0}); + EXPECT_EQ(7, M.argc); + EXPECT_EQ(0, M.argv[M.argc]); + EXPECT_EQ(v0[0], M.argv[0]+s.optoff); + + s.consume(MR{nullptr, 0, 2}); + EXPECT_EQ(7, M.argc); + EXPECT_EQ(0, M.argv[M.argc]); + EXPECT_EQ(v0[0]+2, M.argv[0]+s.optoff); + + s.consume(MR{nullptr, 2, 0}); + EXPECT_EQ(5, M.argc); + EXPECT_EQ(0, M.argv[M.argc]); + EXPECT_EQ(v0[2], M.argv[0]+s.optoff); + + s.consume(MR{nullptr, 0, 2}); + EXPECT_EQ(5, M.argc); + EXPECT_EQ(0, M.argv[M.argc]); + EXPECT_EQ(v0[2]+2, M.argv[0]+s.optoff); + + s.consume(MR{nullptr, 1, 1}); + EXPECT_EQ(4, M.argc); + EXPECT_EQ(0, M.argv[M.argc]); + EXPECT_EQ(v0[3]+1, M.argv[0]+s.optoff); +} + TEST(state, match_long) { to::key k("key", to::key::longfmt); @@ -44,9 +84,13 @@ TEST(state, match_long) { mockargs M("key\0value\0rest\0"); to::state s(M.argc, M.argv); - auto arg = s.match_option(k); - ASSERT_TRUE(arg); - EXPECT_EQ("value"s, *arg); + auto mmr = s.match_option(k); + ASSERT_TRUE(mmr); + EXPECT_EQ("value"s, mmr->argument); + EXPECT_EQ(2u, mmr->shift); + EXPECT_EQ(0u, mmr->offset); + + s.consume(*mmr); EXPECT_EQ("rest"s, M.argv[0]); } @@ -54,8 +98,13 @@ TEST(state, match_long) { mockargs M("key\0value\0rest\0"); to::state s(M.argc, M.argv); - auto arg = s.match_flag(k); - EXPECT_EQ(true, arg); + auto mmr = s.match_flag(k); + ASSERT_TRUE(mmr); + EXPECT_EQ(nullptr, mmr->argument); + EXPECT_EQ(1u, mmr->shift); + EXPECT_EQ(0u, mmr->offset); + + s.consume(*mmr); EXPECT_EQ("value"s, M.argv[0]); } @@ -63,18 +112,21 @@ TEST(state, match_long) { mockargs M("key=value\0rest\0"); to::state s(M.argc, M.argv); - auto arg = s.match_flag(k); - EXPECT_EQ(false, arg); - EXPECT_EQ("key=value"s, M.argv[0]); + auto mmr = s.match_flag(k); + ASSERT_FALSE(mmr); } { mockargs M("key=value\0rest\0"); to::state s(M.argc, M.argv); - auto arg = s.match_option(k); - ASSERT_TRUE(arg); - EXPECT_EQ("value"s, *arg); + auto mmr = s.match_option(k); + ASSERT_TRUE(mmr); + EXPECT_EQ("value"s, mmr->argument); + EXPECT_EQ(1u, mmr->shift); + EXPECT_EQ(0u, mmr->offset); + + s.consume(*mmr); EXPECT_EQ("rest"s, M.argv[0]); } @@ -82,9 +134,8 @@ TEST(state, match_long) { mockargs M("keyvalue\0rest\0"); to::state s(M.argc, M.argv); - auto arg = s.match_option(k); - EXPECT_FALSE(arg); - EXPECT_EQ("keyvalue"s, M.argv[0]); + auto mmr = s.match_option(k); + ASSERT_FALSE(mmr); } } @@ -95,9 +146,13 @@ TEST(state, match_short) { mockargs M("key\0value\0rest\0"); to::state s(M.argc, M.argv); - auto arg = s.match_option(k); - ASSERT_TRUE(arg); - EXPECT_EQ("value"s, *arg); + auto mmr = s.match_option(k); + ASSERT_TRUE(mmr); + EXPECT_EQ("value"s, mmr->argument); + EXPECT_EQ(2u, mmr->shift); + EXPECT_EQ(0u, mmr->offset); + + s.consume(*mmr); EXPECT_EQ("rest"s, M.argv[0]); } @@ -105,8 +160,13 @@ TEST(state, match_short) { mockargs M("key\0value\0rest\0"); to::state s(M.argc, M.argv); - auto arg = s.match_flag(k); - EXPECT_EQ(true, arg); + auto mmr = s.match_flag(k); + ASSERT_TRUE(mmr); + EXPECT_EQ(nullptr, mmr->argument); + EXPECT_EQ(1u, mmr->shift); + EXPECT_EQ(0u, mmr->offset); + + s.consume(*mmr); EXPECT_EQ("value"s, M.argv[0]); } @@ -114,27 +174,24 @@ TEST(state, match_short) { mockargs M("key=value\0rest\0"); to::state s(M.argc, M.argv); - auto arg = s.match_option(k); - EXPECT_FALSE(arg); - EXPECT_EQ("key=value"s, M.argv[0]); + auto mmr = s.match_option(k); + ASSERT_FALSE(mmr); } { mockargs M("key=value\0rest\0"); to::state s(M.argc, M.argv); - auto arg = s.match_flag(k); - EXPECT_EQ(false, arg); - EXPECT_EQ("key=value"s, M.argv[0]); + auto mmr = s.match_option(k); + ASSERT_FALSE(mmr); } { mockargs M("keyvalue\0rest\0"); to::state s(M.argc, M.argv); - auto arg = s.match_option(k); - EXPECT_FALSE(arg); - EXPECT_EQ("keyvalue"s, M.argv[0]); + auto mmr = s.match_option(k); + ASSERT_FALSE(mmr); } } @@ -145,9 +202,13 @@ TEST(state, match_compact) { mockargs M("key\0value\0rest\0"); to::state s(M.argc, M.argv); - auto arg = s.match_option(k); - ASSERT_TRUE(arg); - EXPECT_EQ("value"s, *arg); + auto mmr = s.match_option(k); + ASSERT_TRUE(mmr); + EXPECT_EQ("value"s, mmr->argument); + EXPECT_EQ(2, mmr->shift); + EXPECT_EQ(0, mmr->offset); + + s.consume(*mmr); EXPECT_EQ("rest"s, M.argv[0]); } @@ -155,8 +216,13 @@ TEST(state, match_compact) { mockargs M("key\0value\0rest\0"); to::state s(M.argc, M.argv); - auto arg = s.match_flag(k); - EXPECT_EQ(true, arg); + auto mmr = s.match_flag(k); + ASSERT_TRUE(mmr); + EXPECT_EQ(nullptr, mmr->argument); + EXPECT_EQ(1, mmr->shift); + EXPECT_EQ(0, mmr->offset); + + s.consume(*mmr); EXPECT_EQ("value"s, M.argv[0]); } @@ -164,9 +230,13 @@ TEST(state, match_compact) { mockargs M("key=value\0rest\0"); to::state s(M.argc, M.argv); - auto arg = s.match_option(k); - ASSERT_TRUE(arg); - EXPECT_EQ("=value"s, *arg); + auto mmr = s.match_option(k); + ASSERT_TRUE(mmr); + EXPECT_EQ("=value"s, mmr->argument); + EXPECT_EQ(1, mmr->shift); + EXPECT_EQ(0, mmr->offset); + + s.consume(*mmr); EXPECT_EQ("rest"s, M.argv[0]); } @@ -174,19 +244,29 @@ TEST(state, match_compact) { mockargs M("key=value\0rest\0"); to::state s(M.argc, M.argv); - auto arg = s.match_flag(k); - EXPECT_EQ(true, arg); + auto mmr = s.match_flag(k); + ASSERT_TRUE(mmr); + EXPECT_EQ(nullptr, mmr->argument); + EXPECT_EQ(0, mmr->shift); + EXPECT_EQ(3, mmr->offset); + + s.consume(*mmr); EXPECT_EQ("key=value"s, M.argv[0]); + EXPECT_EQ("=value"s, M.argv[0]+s.optoff); } { mockargs M("keyvalue\0rest\0"); to::state s(M.argc, M.argv); - auto arg = s.match_option(k); - ASSERT_TRUE(arg); - EXPECT_EQ("value"s, *arg); - EXPECT_EQ("rest"s, M.argv[0]); + auto mmr = s.match_option(k); + ASSERT_TRUE(mmr); + EXPECT_EQ("value"s, mmr->argument); + EXPECT_EQ(1, mmr->shift); + EXPECT_EQ(0, mmr->offset); + + s.consume(*mmr); + EXPECT_EQ("rest"s, M.argv[0]+s.optoff); } } @@ -200,11 +280,22 @@ TEST(state, match_multi_compact) { mockargs M("key/one/three/two\0key/four\0rest\0"); to::state s(M.argc, M.argv); - EXPECT_TRUE(s.match_flag(k1)); - EXPECT_FALSE(s.match_flag(k2)); - EXPECT_TRUE(s.match_flag(k3)); - EXPECT_TRUE(s.match_flag(k2)); - EXPECT_TRUE(s.match_flag(k4)); + to::maybe mmr; + + ASSERT_TRUE(mmr = s.match_flag(k1)); + s.consume(*mmr); + + ASSERT_FALSE(mmr = s.match_flag(k2)); + + ASSERT_TRUE(mmr = s.match_flag(k3)); + s.consume(*mmr); + + ASSERT_TRUE(mmr = s.match_flag(k2)); + s.consume(*mmr); + + ASSERT_TRUE(mmr = s.match_flag(k4)); + s.consume(*mmr); + EXPECT_EQ("rest"s, M.argv[0]); } }