/** @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 #include #include #include #include #include #include #include #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, CATEGORY }; enum class arg_value { NONE = 0, REQUIRED, OPTIONAL, ALL, REST }; struct arg_description { virtual ~arg_description() {} virtual arg_type type() const = 0; virtual bool is_arg(string_range name) const = 0; protected: 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; } arg_argument &metavar(string_range str) { p_metavar = std::string{str}; return *this; } string_range get_metavar() const { return p_metavar; } std::size_t get_nargs() const { return p_nargs; } protected: arg_argument(arg_value req = arg_value::NONE, std::size_t nargs = 1): arg_description(), p_valreq(req), p_nargs(nargs) {} arg_argument(std::size_t nargs): arg_description(), p_valreq((nargs > 0) ? arg_value::REQUIRED : arg_value::NONE), p_nargs(nargs) {} std::string p_helpstr, p_metavar; arg_value p_valreq; std::size_t p_nargs; }; struct arg_optional: arg_argument { template friend struct basic_arg_parser; friend struct arg_description_container; arg_type type() const { return arg_type::OPTIONAL; } bool is_arg(string_range name) const { for (auto const &nm: p_names) { if (name == iter(nm)) { return true; } } return false; } arg_value needs_value() const { return p_valreq; } std::size_t used() const { return p_used; } template arg_optional &action(F func) { p_action = func; return *this; } arg_optional &help(string_range str) { arg_argument::help(str); return *this; } arg_optional &metavar(string_range str) { arg_argument::metavar(str); return *this; } arg_optional &limit(std::size_t n) { p_limit = n; return *this; } 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; arg_optional(string_range name, arg_value req, std::size_t nargs = 1): arg_argument(req, nargs) { validate_req(req); p_names.emplace_back(name); } arg_optional(string_range name, std::size_t nargs): arg_argument(nargs) { p_names.emplace_back(name); } arg_optional( string_range name1, string_range name2, arg_value req, std::size_t nargs = 1 ): arg_argument(req, nargs) { validate_req(req); p_names.emplace_back(name1); p_names.emplace_back(name2); } arg_optional(string_range name1, string_range name2, std::size_t nargs): arg_argument(nargs) { p_names.emplace_back(name1); p_names.emplace_back(name2); } void set_values( string_range argname, iterator_range vals ) { if (p_limit && (p_used == p_limit)) { throw arg_error{format( appender(), "argument '%s' can be used at most %d times", argname, p_limit ).get()}; } if (p_action) { p_action(vals); } ++p_used; } private: void validate_req(arg_value req) { switch (req) { case arg_value::NONE: case arg_value::REQUIRED: case arg_value::OPTIONAL: case arg_value::ALL: break; default: throw arg_error{"invalid argument requirement"}; } } std::function)> p_action; std::vector p_names; std::size_t p_used = 0, p_limit = 0; }; struct arg_positional: arg_argument { friend struct arg_description_container; arg_type type() const { return arg_type::POSITIONAL; } bool is_arg(string_range name) const { return (name == ostd::citer(p_name)); } string_range get_name() const { return p_name; } protected: arg_positional() = delete; arg_positional( string_range name, arg_value req = arg_value::REQUIRED, std::size_t nargs = 1 ): arg_argument(req, nargs), p_name(name) {} arg_positional(string_range name, std::size_t nargs): arg_argument(nargs), p_name(name) {} private: std::string p_name; }; struct arg_category: arg_description { friend struct arg_description_container; arg_type type() const { return arg_type::CATEGORY; } bool is_arg(string_range) const { return false; } protected: arg_category() = delete; arg_category( string_range name ): p_name(name) {} private: std::string p_name; }; struct arg_description_container { template arg_optional &add_optional(A &&...args) { arg_description *p = new arg_optional(std::forward(args)...); return static_cast(*p_opts.emplace_back(p)); } template arg_positional &add_positional(A &&...args) { arg_description *p = new arg_positional(std::forward(args)...); return static_cast(*p_opts.emplace_back(p)); } template arg_category &add_category(A &&...args) { arg_description *p = new arg_category(std::forward(args)...); return static_cast(*p_opts.emplace_back(p)); } auto iter() const { return ostd::citer(p_opts); } protected: arg_description_container() {} arg_description *find_arg_ptr(string_range name) { for (auto &p: p_opts) { if (p->is_arg(name)) { return &*p; } } return nullptr; } template AT &find_arg(string_range name) { auto p = static_cast(find_arg_ptr(name)); if (p) { return *p; } throw arg_error{format( appender(), "unknown argument '%s'", name ).get()}; } std::vector> p_opts; }; template struct basic_arg_parser: arg_description_container { basic_arg_parser(string_range progname = string_range{}): arg_description_container(), p_progname(progname) {} void parse(int argc, char **argv) { if (p_progname.empty()) { p_progname = argv[0]; } parse(ostd::iter(&argv[1], &argv[argc])); } template 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) { parse_pos(s); continue; } if (starts_with(s, "--")) { parse_long(s, args); continue; } if ((s.size() > 1) && (s[0] == '-') && (s != "-")) { parse_short(s, args); continue; } parse_pos(s); } } template arg_optional &add_help(OutputRange out, string_range msg) { auto &opt = add_optional("-h", "--help", arg_value::NONE); opt.help(msg); opt.action([this, out = std::move(out)](auto) mutable { this->print_help(out); return true; }); return opt; } arg_optional &add_help(string_range msg) { return add_help(cout.iter(), msg); } template OutputRange &&print_help(OutputRange &&range) { p_helpfmt.format_usage(range); p_helpfmt.format_options(range); return std::forward(range); } void print_help() { print_help(cout.iter()); } arg_argument &get(string_range name) { return find_arg(name); } std::size_t used(string_range name) { auto &arg = find_arg(name); return arg.p_used; } string_range get_progname() const { return p_progname; } private: template void parse_long(string_range arg, R &args) { std::optional val; if (auto sv = find(arg, '='); !sv.empty()) { arg = arg.slice(0, arg.size() - sv.size()); sv.pop_front(); val = sv; } parse_arg(arg, std::move(val), args); } template void parse_short(string_range arg, R &args) { std::optional val; if (arg.size() > 2) { val = arg.slice(2); arg = arg.slice(0, 2); } parse_arg(arg, std::move(val), args); } template void parse_arg( string_range arg, std::optional val, R &args ) { bool arg_val = false; std::string argname{arg}; auto &desc = find_arg(arg); args.pop_front(); auto needs = desc.needs_value(); if (needs == arg_value::NONE) { if (val) { throw arg_error{format( appender(), "argument '%s' takes no value", argname ).get()}; } desc.set_values(argname, nullptr); return; } if (!val) { if (args.empty()) { if (needs == arg_value::REQUIRED) { throw arg_error{format( appender(), "argument '%s' needs a value", argname ).get()}; } desc.set_values(argname, nullptr); return; } string_range tval = args.front(); if ((needs != arg_value::OPTIONAL) || !find_arg_ptr(tval)) { val = tval; arg_val = true; } } if (val) { desc.set_values(argname, ostd::iter({ *val })); if (arg_val) { args.pop_front(); } } else { desc.set_values(argname, nullptr); } } void parse_pos(string_range) { } std::string p_progname; HelpFormatter p_helpfmt{*this}; }; struct default_help_formatter { default_help_formatter(basic_arg_parser &p): p_parser(p) {} template 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 void format_options(OutputRange &out) { std::size_t opt_namel = 0, pos_namel = 0; for (auto &p: p_parser.iter()) { auto cs = counting_sink(noop_sink()); switch (p->type()) { case arg_type::OPTIONAL: { format_option(cs, static_cast(*p)); opt_namel = std::max(opt_namel, cs.get_written()); break; } case arg_type::POSITIONAL: format_option(cs, static_cast(*p)); pos_namel = std::max(pos_namel, cs.get_written()); break; default: break; } } std::size_t maxpad = std::max(opt_namel, pos_namel); 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; } format(out, " "); auto &parg = static_cast(*p.get()); 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(*p.get()); auto cr = counting_sink(out); format_option(cr, oarg); out = std::move(cr.get_range()); write_help(out, oarg, cr.get_written()); } } } template 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; } string_range mtr = s; while (!mtr.empty() && (mtr.front() == '-')) { mtr.pop_front(); } 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::REQUIRED: format(out, " %s", mt); break; case arg_value::OPTIONAL: format(out, " [%s]", mt); break; case arg_value::ALL: if (arg.get_nargs() > 0) { format(out, " %s", mt); } else { format(out, " [%s]", mt); } break; default: break; } first = false; } } template void format_option(OutputRange &out, arg_positional const &arg) { auto mt = arg.get_metavar(); if (mt.empty()) { mt = arg.get_name(); } format(out, mt); } private: basic_arg_parser &p_parser; }; using arg_parser = basic_arg_parser; template auto arg_print_help(OutputRange o, arg_parser &p) { return [o = std::move(o), &p](iterator_range) mutable { p.print_help(o); }; }; auto arg_print_help(arg_parser &p) { return arg_print_help(cout.iter(), p); } template auto arg_store_const(T &&val, U &ref) { return [val, &ref](iterator_range) mutable { ref = std::move(val); }; } template auto arg_store_str(T &ref) { return [&ref](iterator_range r) mutable { ref = T{r[0]}; }; } auto arg_store_true(bool &ref) { return arg_store_const(true, ref); } auto arg_store_false(bool &ref) { return arg_store_const(false, ref); } /** @} */ } /* namespace ostd */ #endif /** @} */