Skip to content

Commit

Permalink
Support for optional arguments with to::lax (#19)
Browse files Browse the repository at this point in the history
* Separate argv consumption from option matching in `state` and `counted_option` classes.
* Implement `to::lax`.
* Update unit tests against new internal API.
* Add example 6 that demonstrates `to::lax`.
* Add unit tests for `to::lax`.
* Update documentation and add examples to explain `to::lax` and the use of modal options to implement options with a count of arguments.
* Add `ex7-run` target to Makefile.
* Edit examples for consistency.
* Exclude CI with clang++ and c++20 on ubuntu-22.04 image in order to avoid triggering actions/runner-images#8659
  • Loading branch information
halfflat authored May 7, 2024
1 parent 7e6d707 commit d02b045
Show file tree
Hide file tree
Showing 15 changed files with 436 additions and 142 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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++"
Expand Down
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

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.)
8 changes: 5 additions & 3 deletions ex/ex1-parse.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 6 additions & 4 deletions ex/ex1-run.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions ex/ex2-parse.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions ex/ex2-run.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 5 additions & 4 deletions ex/ex3-parse.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 5 additions & 4 deletions ex/ex3-run.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
48 changes: 48 additions & 0 deletions ex/ex6-run.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#include <iostream>
#include <string>

#include <tinyopt/tinyopt.h>

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<const char*, const char*> 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<int>{}), "-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;
}
}
47 changes: 47 additions & 0 deletions ex/ex7-run.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#include <iostream>
#include <iterator>
#include <vector>

#include <tinyopt/tinyopt.h>

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<std::vector<int>> 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<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;
}
}
Loading

0 comments on commit d02b045

Please sign in to comment.