/** @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 "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_parser; struct arg_description { friend struct arg_parser; friend struct arg_description_container; 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 { friend struct arg_parser; friend struct arg_description_container; arg_argument &help(string_range str) { p_helpstr = std::string{str}; return *this; } protected: arg_argument(arg_value req = arg_value::NONE, int nargs = 1): arg_description(), p_valreq(req), p_nargs(nargs) {} arg_argument(int nargs): arg_description(), p_valreq((nargs > 0) ? arg_value::REQUIRED : ((nargs < 0) ? arg_value::ALL : arg_value::NONE) ), p_nargs(nargs) {} std::string p_helpstr; arg_value p_valreq; int p_nargs; }; struct arg_optional: arg_argument { friend struct 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; } bool used() const { return p_used; } template arg_optional &action(F func) { p_action = [this, func = std::move(func)]( iterator_range vals ) mutable { func(vals); p_used = true; }; return *this; } arg_optional &help(string_range str) { arg_argument::help(str); return *this; } arg_optional &add_name(string_range name) { p_names.emplace_back(name); return *this; } protected: arg_optional() = delete; arg_optional(string_range name, arg_value req, int nargs = 1): arg_argument(req, nargs) { p_names.emplace_back(name); } arg_optional(string_range name, int nargs): arg_argument(nargs) { p_names.emplace_back(name); } arg_optional( string_range name1, string_range name2, arg_value req, int nargs = 1 ): arg_argument(req, nargs) { p_names.emplace_back(name1); p_names.emplace_back(name2); } arg_optional(string_range name1, string_range name2, int nargs): arg_argument(nargs) { p_names.emplace_back(name1); p_names.emplace_back(name2); } void set_values(iterator_range vals) { if (p_action) { p_action(vals); } else { p_used = true; } } private: std::function)> p_action; std::vector p_names; bool p_used = false; }; struct arg_positional: arg_argument { friend struct arg_parser; 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)); } protected: arg_positional() = delete; arg_positional( string_range name, arg_value req = arg_value::REQUIRED, int nargs = 1 ): arg_argument(req, nargs), p_name(name) {} arg_positional(string_range name, int nargs): arg_argument(nargs), p_name(name) {} private: std::string p_name; }; struct arg_category: arg_description { friend struct arg_parser; 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)); } 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; }; struct arg_parser: arg_description_container { 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(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) { print_usage_impl(range); print_help_impl(range); return std::forward(range); } void print_help() { print_help(cout.iter()); } arg_argument &get(string_range name) { return find_arg(name); } bool used(string_range name) { auto &arg = find_arg(name); return arg.p_used; } private: template void print_usage_impl(OR &out) { string_range progname = p_progname; if (progname.empty()) { progname = "program"; } format(out, "usage: %s [opts] [args]\n", progname); } template void print_help_impl(OR &out) { std::size_t opt_namel = 0, pos_namel = 0; for (auto &p: p_opts) { switch (p->type()) { case arg_type::OPTIONAL: { auto &opt = static_cast(*p); std::size_t nl = 0; for (auto &s: opt.p_names) { nl += s.size(); } nl += 2 * (opt.p_names.size() - 1); opt_namel = std::max(opt_namel, nl); break; } case arg_type::POSITIONAL: pos_namel = std::max( pos_namel, static_cast(*p).p_name.size() ); break; default: break; } } std::size_t maxpad = std::max(opt_namel, pos_namel); if (pos_namel) { format(out, "\npositional arguments:\n"); for (auto &p: p_opts) { if (p->type() != arg_type::POSITIONAL) { continue; } auto &parg = static_cast(*p.get()); format(out, " %s", parg.p_name); if (parg.p_helpstr.empty()) { out.put('\n'); } else { std::size_t nd = maxpad - parg.p_name.size() + 2; for (std::size_t i = 0; i < nd; ++i) { out.put(' '); } format(out, "%s\n", parg.p_helpstr); } } } if (opt_namel) { format(out, "\noptional arguments:\n"); for (auto &p: p_opts) { if (p->type() != arg_type::OPTIONAL) { continue; } auto &parg = static_cast(*p.get()); format(out, " "); std::size_t nd = 0; for (auto &s: parg.p_names) { if (nd) { nd += 2; format(out, ", "); } nd += s.size(); format(out, "%s", s); } if (parg.p_helpstr.empty()) { out.put('\n'); } else { nd = maxpad - nd + 2; for (std::size_t i = 0; i < nd; ++i) { out.put(' '); } format(out, "%s\n", parg.p_helpstr); } } } } 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(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(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(iter({ *val })); if (arg_val) { args.pop_front(); } } else { desc.set_values(nullptr); } } void parse_pos(string_range) { } std::string p_progname; }; 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 /** @} */