Skip to content

Commit

Permalink
* Add unit tests for to::lax.
Browse files Browse the repository at this point in the history
* Update documentation and add examples to explain to::lax and the use
  of modal options to implement options with a count of arguments.
  • Loading branch information
halfflat committed May 1, 2024
1 parent 0c3e0eb commit be8b90e
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 15 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ ex5-run: ex5-run.o
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)

Expand Down
63 changes: 58 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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<void>`.

`maybe<V>` values support basic monadic-like functionality via `operator<<`.
* If `x` is an lvalue and `m` is of type `maybe<U>`, then
`x << m` has type `maybe<V>` (`V` is the type of `x=*m`) and assigns `m.value()` to `x`
* If `x` is an lvalue and `m` is of type `maybe<U>`, then
`x << m` has type `maybe<V>` 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<U>`, then
Expand Down Expand Up @@ -223,7 +223,7 @@ An alternative prefix to "Usage: " can be supplied optionally.
A parser is a function or functional object with signature `maybe<X> (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.

Expand Down Expand Up @@ -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 `|`.

Expand Down Expand Up @@ -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<int> 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
Expand Down Expand Up @@ -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<std::vector<int>> 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<int> 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.)
59 changes: 59 additions & 0 deletions ex/ex7-run.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#include <iostream>
#include <iterator>
#include <vector>

#include <tinyopt/tinyopt.h>

const char* usage_str =
"[-n [ INT [ INT [ INT ] ] ] | -a | -b ] ...\n"
"\n"
"Collect vectors of up to 3 integers as multiple arguments to the -n option.\n"
"Count occurances of flags -a and -b.\n";

int main(int argc, char** argv) {
try {
auto help = [argv0 = argv[0]] { to::usage(argv0, usage_str); };

std::vector<std::vector<int>> nss;
auto new_ns = [&]() { nss.push_back({}); return true; };
auto push_ns = [&](int n) { nss.back().push_back(n); return true; };

int a = 0, b = 0;

#if 0
to::option opts[] = {
{ to::action(help), "-h", "--help", to::exit },
{ to::increment(a), to::then(0), "-a", to::flag },
{ to::increment(b), to::then(0), "-b", to::flag },
{ to::action(new_ns), to::then(1), "-n", to::flag },
{ to::action(push_ns), to::when(1) }
};
#endif
auto gt0 = [](int m) { return m>0; };
auto decrement = [](int m) { return m-1; };

to::option opts[] = {
{ to::action(help), "-h", "--help", to::exit },
{ to::increment(a), to::then(0), "-a", to::flag },
{ to::increment(b), to::then(0), "-b", to::flag },
{ 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("unrecogonized argument", argv[1]);

std::cout << "a count: " << a << "\nb count: " << b << "\n";
std::cout << "ns:\n";
for (auto& ns: nss) {
std::cout << "{ ";
std::ostream_iterator<int> 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;
}
}
51 changes: 41 additions & 10 deletions test/test_option.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -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());
Expand All @@ -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<std::string>;
auto key_labels = [](const to::option& o) {
svec labels;
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}

0 comments on commit be8b90e

Please sign in to comment.