libostd/ostd/argparse.hh

865 lines
23 KiB
C++
Raw Normal View History

/** @addtogroup Utilities
* @{
*/
/** @file argúparse.hh
*
* @brief Portable argument parsing.
*
* Provides a powerful argument parser that can handle a wide variety of
* cases, including POSIX and GNU argument ordering, different argument
* formats, optional values and type conversions.
*
* @copyright See COPYING.md in the project tree for further information.
*/
#ifndef OSTD_ARGPARSE_HH
#define OSTD_ARGPARSE_HH
2017-05-19 01:37:45 +02:00
#include <cstdio>
2017-05-17 20:56:01 +02:00
#include <cctype>
#include <algorithm>
2017-05-16 19:25:20 +02:00
#include <optional>
#include <vector>
#include <memory>
#include <stdexcept>
#include <utility>
2017-05-19 01:37:45 +02:00
#include <functional>
#include "ostd/algorithm.hh"
#include "ostd/format.hh"
#include "ostd/string.hh"
#include "ostd/io.hh"
namespace ostd {
/** @addtogroup Utilities
* @{
*/
struct arg_error: std::runtime_error {
using std::runtime_error::runtime_error;
};
enum class arg_type {
OPTIONAL = 0,
POSITIONAL,
GROUP
};
enum class arg_value {
EXACTLY,
OPTIONAL,
ALL,
REST
};
2017-05-15 18:49:59 +02:00
struct arg_description {
2017-05-21 15:40:52 +02:00
friend struct arg_description_container;
2017-05-15 18:49:59 +02:00
virtual ~arg_description() {}
virtual arg_type type() const = 0;
2017-05-15 18:49:59 +02:00
protected:
2017-05-21 15:40:52 +02:00
virtual arg_description *find_arg(string_range, std::optional<arg_type>) {
return nullptr;
}
2017-05-15 18:49:59 +02:00
arg_description() {}
};
struct arg_argument: arg_description {
arg_argument &help(string_range str) {
p_helpstr = std::string{str};
return *this;
}
string_range get_help() const {
return p_helpstr;
}
2017-05-17 20:56:01 +02:00
arg_argument &metavar(string_range str) {
p_metavar = std::string{str};
return *this;
}
string_range get_metavar() const {
return p_metavar;
}
arg_value needs_value() const {
return p_valreq;
}
2017-05-17 20:56:01 +02:00
std::size_t get_nargs() const {
return p_nargs;
}
protected:
arg_argument(arg_value req, std::size_t nargs):
2017-05-15 18:49:59 +02:00
arg_description(), p_valreq(req), p_nargs(nargs)
{}
2017-05-17 20:56:01 +02:00
arg_argument(std::size_t nargs):
2017-05-15 18:49:59 +02:00
arg_description(),
p_valreq(arg_value::EXACTLY),
2017-05-17 20:56:01 +02:00
p_nargs(nargs)
{}
2017-05-17 20:56:01 +02:00
std::string p_helpstr, p_metavar;
arg_value p_valreq;
2017-05-17 20:56:01 +02:00
std::size_t p_nargs;
};
struct arg_optional: arg_argument {
template<typename HelpFormatter>
friend struct basic_arg_parser;
friend struct arg_description_container;
friend struct arg_group;
arg_type type() const {
return arg_type::OPTIONAL;
}
2017-05-17 00:38:12 +02:00
std::size_t used() const {
2017-05-16 00:22:46 +02:00
return p_used;
}
template<typename F>
arg_optional &action(F func) {
2017-05-17 00:38:12 +02:00
p_action = func;
return *this;
}
arg_optional &help(string_range str) {
arg_argument::help(str);
return *this;
}
2017-05-18 03:54:27 +02:00
arg_optional &metavar(string_range str) {
arg_argument::metavar(str);
return *this;
}
arg_optional &limit(std::size_t n) {
p_limit = n;
return *this;
}
2017-05-16 19:25:20 +02:00
arg_optional &add_name(string_range name) {
p_names.emplace_back(name);
return *this;
}
auto get_names() const {
return iter(p_names);
}
protected:
arg_optional() = delete;
2017-05-16 19:25:20 +02:00
2017-05-17 20:56:01 +02:00
arg_optional(string_range name, arg_value req, std::size_t nargs = 1):
2017-05-16 19:25:20 +02:00
arg_argument(req, nargs)
{
validate_req(req);
2017-05-16 19:25:20 +02:00
p_names.emplace_back(name);
}
2017-05-17 20:56:01 +02:00
arg_optional(string_range name, std::size_t nargs):
2017-05-16 19:25:20 +02:00
arg_argument(nargs)
{
p_names.emplace_back(name);
}
arg_optional(
2017-05-17 20:56:01 +02:00
string_range name1, string_range name2, arg_value req,
std::size_t nargs = 1
):
2017-05-16 19:25:20 +02:00
arg_argument(req, nargs)
{
validate_req(req);
2017-05-16 19:25:20 +02:00
p_names.emplace_back(name1);
p_names.emplace_back(name2);
}
2017-05-17 20:56:01 +02:00
arg_optional(string_range name1, string_range name2, std::size_t nargs):
2017-05-16 19:25:20 +02:00
arg_argument(nargs)
{
p_names.emplace_back(name1);
p_names.emplace_back(name2);
}
2017-05-21 15:40:52 +02:00
arg_description *find_arg(string_range name, std::optional<arg_type> tp) {
if (tp && (*tp != type())) {
return nullptr;
}
for (auto const &nm: p_names) {
if (name == iter(nm)) {
return this;
}
}
return nullptr;
}
2017-05-18 03:54:27 +02:00
void set_values(
string_range argname, iterator_range<string_range const *> vals
) {
if (p_limit && (p_used == p_limit)) {
throw arg_error{format(
appender<std::string>(),
"argument '%s' can be used at most %d times", argname, p_limit
).get()};
}
++p_used;
if (p_action) {
p_action(vals);
}
}
private:
void validate_req(arg_value req) {
switch (req) {
case arg_value::EXACTLY:
case arg_value::OPTIONAL:
case arg_value::ALL:
break;
default:
throw arg_error{"invalid argument requirement"};
}
}
std::function<void(iterator_range<string_range const *>)> p_action;
2017-05-16 19:25:20 +02:00
std::vector<std::string> p_names;
2017-05-18 03:54:27 +02:00
std::size_t p_used = 0, p_limit = 0;
};
struct arg_positional: arg_argument {
2017-05-21 02:55:25 +02:00
template<typename HelpFormatter>
friend struct basic_arg_parser;
friend struct arg_description_container;
arg_type type() const {
return arg_type::POSITIONAL;
}
string_range get_name() const {
return p_name;
}
2017-05-21 02:55:25 +02:00
template<typename F>
arg_positional &action(F func) {
p_action = func;
return *this;
}
bool used() const {
return p_used;
}
protected:
arg_positional() = delete;
arg_positional(
string_range name, arg_value req = arg_value::EXACTLY,
2017-05-17 20:56:01 +02:00
std::size_t nargs = 1
):
arg_argument(req, nargs),
p_name(name)
{}
2017-05-17 20:56:01 +02:00
arg_positional(string_range name, std::size_t nargs):
2017-05-15 18:49:59 +02:00
arg_argument(nargs),
p_name(name)
{}
2017-05-21 15:40:52 +02:00
arg_description *find_arg(string_range name, std::optional<arg_type> tp) {
if ((tp && (*tp != type())) || (name != ostd::citer(p_name))) {
return nullptr;
}
return this;
}
2017-05-21 02:55:25 +02:00
void set_values(iterator_range<string_range const *> vals) {
if (p_action) {
p_action(vals);
}
p_used = true;
}
private:
2017-05-21 02:55:25 +02:00
std::function<void(iterator_range<string_range const *>)> p_action;
std::string p_name;
2017-05-21 02:55:25 +02:00
bool p_used = false;
};
struct arg_group: arg_description {
friend struct arg_description_container;
arg_type type() const {
return arg_type::GROUP;
}
string_range get_name() const {
return p_name;
}
template<typename ...A>
arg_optional &add_optional(A &&...args) {
arg_optional *o = new arg_optional(std::forward<A>(args)...);
return *p_opts.emplace_back(o);
}
auto iter() const {
return ostd::citer(p_opts);
}
protected:
arg_group() = delete;
arg_group(string_range name):
p_name(name)
{}
arg_description *find_arg(string_range name, std::optional<arg_type> tp) {
for (auto &opt: p_opts) {
if (auto *p = opt->find_arg(name, tp); p) {
return p;
}
}
return nullptr;
}
private:
std::string p_name;
std::vector<std::unique_ptr<arg_optional>> p_opts;
};
struct arg_description_container {
template<typename ...A>
arg_optional &add_optional(A &&...args) {
arg_description *p = new arg_optional(std::forward<A>(args)...);
return static_cast<arg_optional &>(*p_opts.emplace_back(p));
}
template<typename ...A>
arg_positional &add_positional(A &&...args) {
arg_description *p = new arg_positional(std::forward<A>(args)...);
return static_cast<arg_positional &>(*p_opts.emplace_back(p));
}
template<typename ...A>
arg_group &add_group(A &&...args) {
arg_description *p = new arg_group(std::forward<A>(args)...);
return static_cast<arg_group &>(*p_opts.emplace_back(p));
}
auto iter() const {
return ostd::citer(p_opts);
}
protected:
arg_description_container() {}
template<typename AT>
AT &find_arg(string_range name) {
2017-05-21 15:08:23 +02:00
for (auto &p: p_opts) {
2017-05-21 15:40:52 +02:00
auto *pp = p->find_arg(name, std::nullopt);
if (!pp) {
continue;
}
if (auto *r = dynamic_cast<AT *>(pp); r) {
return *r;
2017-05-21 15:08:23 +02:00
}
2017-05-21 15:40:52 +02:00
break;
}
throw arg_error{format(
appender<std::string>(), "unknown argument '%s'", name
).get()};
}
std::vector<std::unique_ptr<arg_description>> p_opts;
};
template<typename HelpFormatter>
struct basic_arg_parser: arg_description_container {
private:
struct parse_stop {};
public:
basic_arg_parser(
string_range progname = string_range{}, bool posix = false
):
arg_description_container(), p_progname(progname), p_posix(posix)
{}
void parse(int argc, char **argv) {
if (p_progname.empty()) {
p_progname = argv[0];
}
parse(ostd::iter(&argv[1], &argv[argc]));
}
template<typename InputRange>
void parse(InputRange args) {
bool allow_optional = true;
while (!args.empty()) {
string_range s{args.front()};
if (s == "--") {
args.pop_front();
allow_optional = false;
continue;
}
if (allow_optional && is_optarg(s)) {
try {
parse_opt(s, args);
} catch (parse_stop) {
return;
}
continue;
}
if (p_posix) {
allow_optional = false;
}
2017-05-21 02:55:25 +02:00
parse_pos(s, args, allow_optional);
}
for (auto &p: iter()) {
if (p->type() != arg_type::POSITIONAL) {
continue;
}
arg_positional &desc = static_cast<arg_positional &>(*p);
auto needs = desc.needs_value();
auto nargs = desc.get_nargs();
if ((needs != arg_value::EXACTLY) && (needs != arg_value::ALL)) {
continue;
}
if (!nargs || desc.used()) {
continue;
}
throw arg_error{"too few arguments"};
}
}
template<typename OutputRange>
arg_optional &add_help(OutputRange out, string_range msg) {
auto &opt = add_optional("-h", "--help", 0);
opt.help(msg);
opt.action([this, out = std::move(out)](auto) mutable {
this->print_help(out);
this->stop_parsing();
});
return opt;
}
arg_optional &add_help(string_range msg) {
return add_help(cout.iter(), msg);
}
template<typename OutputRange>
OutputRange &&print_help(OutputRange &&range) {
p_helpfmt.format_usage(range);
p_helpfmt.format_options(range);
return std::forward<OutputRange>(range);
}
void print_help() {
print_help(cout.iter());
}
arg_argument &get(string_range name) {
return find_arg<arg_argument>(name);
}
string_range get_progname() const {
return p_progname;
}
bool posix_ordering() const {
return p_posix;
}
bool posix_ordering(bool v) {
return std::exchange(p_posix, v);
}
void stop_parsing() {
throw parse_stop{};
}
private:
static bool is_optarg(string_range arg) {
return (!arg.empty() && (arg[0] == '-') && (arg != "-"));
}
template<typename R>
void parse_opt(string_range argr, R &args) {
std::vector<std::string> vals;
if (auto sv = find(argr, '='); !sv.empty()) {
argr = argr.slice(0, argr.size() - sv.size());
sv.pop_front();
vals.emplace_back(sv);
}
args.pop_front();
std::string arg{argr};
auto &desc = find_arg<arg_optional>(arg);
auto needs = desc.needs_value();
auto nargs = desc.get_nargs();
/* optional argument takes no values */
if ((needs == arg_value::EXACTLY) && !nargs) {
/* value was provided through = */
if (!vals.empty()) {
throw arg_error{format(
appender<std::string>(), "argument '%s' takes no value",
arg
).get()};
}
desc.set_values(arg, nullptr);
return;
}
if (
vals.empty() ||
(needs == arg_value::ALL) ||
((needs == arg_value::EXACTLY) && (nargs > 1))
) {
auto rargs = nargs;
if ((needs == arg_value::EXACTLY) && !vals.empty()) {
--rargs;
}
for (;;) {
bool pval = !args.empty() && !is_optarg(args.front());
if ((needs == arg_value::EXACTLY) && rargs && !pval) {
throw arg_error{format(
appender<std::string>(),
"argument '%s' needs exactly %d values",
arg, nargs
).get()};
}
if (!pval || ((needs == arg_value::EXACTLY) && !rargs)) {
break;
}
vals.emplace_back(args.front());
args.pop_front();
if (rargs) {
--rargs;
}
}
}
if ((needs == arg_value::ALL) && (nargs > vals.size())) {
throw arg_error{format(
appender<std::string>(),
"argument '%s' needs at least %d values", arg, nargs
).get()};
}
if (!vals.empty()) {
std::vector<string_range> srvals;
for (auto const &s: vals) {
srvals.push_back(s);
}
desc.set_values(
arg, ostd::iter(&srvals[0], &srvals[srvals.size()])
);
} else {
desc.set_values(arg, nullptr);
}
}
2017-05-21 02:55:25 +02:00
template<typename R>
void parse_pos(string_range argr, R &args, bool allow_opt) {
arg_positional *descp = nullptr;
for (auto &popt: p_opts) {
if (popt->type() != arg_type::POSITIONAL) {
continue;
}
arg_positional &o = *static_cast<arg_positional *>(popt.get());
if (o.used()) {
continue;
}
descp = &o;
break;
}
if (!descp) {
throw arg_error{format(
appender<std::string>(), "unexpected argument '%s'", argr
).get()};
}
arg_positional &desc = *descp;
auto needs = desc.needs_value();
auto nargs = desc.get_nargs();
std::vector<std::string> vals;
vals.emplace_back(argr);
args.pop_front();
if (needs == arg_value::REST) {
for (; !args.empty(); args.pop_front()) {
vals.emplace_back(args.front());
}
} else if (needs == arg_value::ALL) {
for (; !args.empty(); args.pop_front()) {
string_range v = args.front();
if (allow_opt && is_optarg(v)) {
break;
}
vals.emplace_back(v);
}
if (nargs > vals.size()) {
throw arg_error{format(
appender<std::string>(),
"positional argument '%s' needs at least %d values",
desc.get_name(), nargs
).get()};
}
} else if ((needs == arg_value::EXACTLY) && (nargs > 1)) {
auto reqargs = nargs - 1;
while (reqargs) {
if (args.empty() || (allow_opt && is_optarg(args.front()))) {
throw arg_error{format(
appender<std::string>(),
"positional argument '%s' needs exactly %d values",
desc.get_name(), nargs
).get()};
}
vals.emplace_back(args.front());
args.pop_front();
}
} /* else is OPTIONAL and we already have an arg */
std::vector<string_range> srvals;
for (auto const &s: vals) {
srvals.push_back(s);
}
desc.set_values(ostd::iter(&srvals[0], &srvals[srvals.size()]));
}
std::string p_progname;
HelpFormatter p_helpfmt{*this};
bool p_posix = false;
};
struct default_help_formatter {
default_help_formatter(basic_arg_parser<default_help_formatter> &p):
p_parser(p)
{}
template<typename OutputRange>
void format_usage(OutputRange &out) {
string_range progname = p_parser.get_progname();
if (progname.empty()) {
progname = "program";
}
format(out, "usage: %s [opts] [args]\n", progname);
}
template<typename OutputRange>
void format_options(OutputRange &out) {
std::size_t opt_namel = 0, pos_namel = 0, grp_namel = 0;
for (auto &p: p_parser.iter()) {
2017-05-17 20:56:01 +02:00
auto cs = counting_sink(noop_sink<char>());
switch (p->type()) {
case arg_type::OPTIONAL:
2017-05-17 20:56:01 +02:00
format_option(cs, static_cast<arg_optional &>(*p));
opt_namel = std::max(opt_namel, cs.get_written());
break;
case arg_type::POSITIONAL:
2017-05-17 20:56:01 +02:00
format_option(cs, static_cast<arg_positional &>(*p));
pos_namel = std::max(pos_namel, cs.get_written());
break;
case arg_type::GROUP:
for (auto &sp: static_cast<arg_group &>(*p).iter()) {
auto ccs = cs;
format_option(ccs, *sp);
grp_namel = std::max(grp_namel, ccs.get_written());
}
break;
default:
break;
}
}
std::size_t maxpad = std::max({opt_namel, pos_namel, grp_namel});
2017-05-17 20:56:01 +02:00
auto write_help = [maxpad](
auto &out, arg_argument &arg, std::size_t len
) {
auto help = arg.get_help();
if (help.empty()) {
out.put('\n');
} else {
std::size_t nd = maxpad - len + 2;
for (std::size_t i = 0; i < nd; ++i) {
out.put(' ');
}
format(out, "%s\n", help);
}
};
if (pos_namel) {
format(out, "\nPositional arguments:\n");
for (auto &p: p_parser.iter()) {
if (p->type() != arg_type::POSITIONAL) {
continue;
}
2017-05-17 20:56:01 +02:00
format(out, " ");
auto &parg = static_cast<arg_positional &>(*p);
2017-05-17 20:56:01 +02:00
auto cr = counting_sink(out);
format_option(cr, parg);
out = std::move(cr.get_range());
write_help(out, parg, cr.get_written());
}
}
if (opt_namel) {
format(out, "\nOptional arguments:\n");
for (auto &p: p_parser.iter()) {
if (p->type() != arg_type::OPTIONAL) {
continue;
}
format(out, " ");
auto &oarg = static_cast<arg_optional &>(*p);
2017-05-17 20:56:01 +02:00
auto cr = counting_sink(out);
format_option(cr, oarg);
out = std::move(cr.get_range());
write_help(out, oarg, cr.get_written());
}
}
for (auto &gp: p_parser.iter()) {
if (gp->type() != arg_type::GROUP) {
continue;
}
auto &garg = static_cast<arg_group &>(*gp);
format(out, "\n%s:\n", garg.get_name());
for (auto &p: garg.iter()) {
format(out, " ");
auto cr = counting_sink(out);
format_option(cr, *p);
out = std::move(cr.get_range());
write_help(out, *p, cr.get_written());
}
}
2017-05-17 20:56:01 +02:00
}
template<typename OutputRange>
void format_option(OutputRange &out, arg_optional const &arg) {
auto names = arg.get_names();
std::string mt{arg.get_metavar()};
if (mt.empty()) {
for (auto &s: names) {
if (!starts_with(s, "--")) {
continue;
}
2017-05-17 20:56:01 +02:00
string_range mtr = s;
while (!mtr.empty() && (mtr.front() == '-')) {
mtr.pop_front();
}
2017-05-17 20:56:01 +02:00
mt = std::string{mtr};
break;
}
if (mt.empty()) {
mt = "VALUE";
} else {
std::transform(mt.begin(), mt.end(), mt.begin(), toupper);
}
}
bool first = true;
for (auto &s: names) {
if (!first) {
format(out, ", ");
}
format(out, s);
switch (arg.needs_value()) {
case arg_value::EXACTLY: {
for (auto nargs = arg.get_nargs(); nargs; --nargs) {
format(out, " %s", mt);
}
2017-05-17 20:56:01 +02:00
break;
}
2017-05-17 20:56:01 +02:00
case arg_value::OPTIONAL:
format(out, " [%s]", mt);
break;
case arg_value::ALL:
for (auto nargs = arg.get_nargs(); nargs; --nargs) {
2017-05-17 20:56:01 +02:00
format(out, " %s", mt);
}
format(out, " [%s ...]", mt);
2017-05-17 20:56:01 +02:00
break;
default:
break;
}
2017-05-17 20:56:01 +02:00
first = false;
}
}
template<typename OutputRange>
void format_option(OutputRange &out, arg_positional const &arg) {
auto mt = arg.get_metavar();
if (mt.empty()) {
mt = arg.get_name();
}
2017-05-17 20:56:01 +02:00
format(out, mt);
}
private:
basic_arg_parser<default_help_formatter> &p_parser;
};
using arg_parser = basic_arg_parser<default_help_formatter>;
template<typename OutputRange>
auto arg_print_help(OutputRange o, arg_parser &p) {
return [o = std::move(o), &p](iterator_range<string_range const *>)
mutable
{
p.print_help(o);
p.stop_parsing();
};
};
auto arg_print_help(arg_parser &p) {
return arg_print_help(cout.iter(), p);
}
2017-05-16 00:22:46 +02:00
template<typename T, typename U>
auto arg_store_const(T &&val, U &ref) {
return [val, &ref](iterator_range<string_range const *>) mutable {
ref = std::move(val);
};
}
template<typename T>
2017-05-16 00:22:46 +02:00
auto arg_store_str(T &ref) {
return [&ref](iterator_range<string_range const *> r) mutable {
ref = T{r[0]};
};
}
2017-05-16 00:22:46 +02:00
auto arg_store_true(bool &ref) {
return arg_store_const(true, ref);
}
2017-05-16 00:22:46 +02:00
auto arg_store_false(bool &ref) {
return arg_store_const(false, ref);
}
2017-05-19 01:37:45 +02:00
template<typename ...A>
auto arg_store_format(string_range fmt, A &...args) {
/* TODO: use ostd::format once it supports reading */
return [fmts = std::string{fmt}, argst = std::tie(args...)](
iterator_range<string_range const *> r
) mutable {
std::apply([&fmts, istr = std::string{r[0]}](auto &...refs) {
if (sscanf(istr.data(), fmts.data(), &refs...) != sizeof...(A)) {
throw arg_error{format(
appender<std::string>(),
"argument requires format '%s' (got '%s')", fmts, istr
).get()};
}
}, argst);
};
}
/** @} */
} /* namespace ostd */
#endif
/** @} */