diff --git a/ostd/argparse.hh b/ostd/argparse.hh new file mode 100644 index 0000000..5de8715 --- /dev/null +++ b/ostd/argparse.hh @@ -0,0 +1,298 @@ +/** @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 "ostd/algorithm.hh" +#include "ostd/format.hh" +#include "ostd/string.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 +}; + +struct arg_parser; + +struct arg_argument { + friend struct arg_parser; + + virtual ~arg_argument() {} + + virtual arg_type type() const = 0; + + virtual bool is_arg(string_range name) const = 0; + +protected: + arg_argument() {} +}; + +struct arg_optional: arg_argument { + friend struct arg_parser; + + arg_type type() const { + return arg_type::OPTIONAL; + } + + bool is_arg(string_range name) const { + if (starts_with(name, "--")) { + return name.slice(2) == ostd::citer(p_lname); + } + if (name[0] == '-') { + return name[1] == p_sname; + } + return false; + } + + arg_value needs_value() const { + return p_valreq; + } + + void set_value(string_range val) { + p_value = std::string{val}; + } + +protected: + arg_optional(char short_name, string_range long_name, arg_value req): + p_lname(long_name), p_valreq(req), p_sname(short_name) + {} + +private: + std::optional p_value; + std::string p_lname; + arg_value p_valreq; + char p_sname; +}; + +struct arg_positional: arg_argument { + friend struct arg_parser; + + arg_type type() const { + return arg_type::POSITIONAL; + } + + bool is_arg(string_range) const { + return false; + } +}; + +struct arg_category: arg_argument { + friend struct arg_parser; + + arg_type type() const { + return arg_type::CATEGORY; + } + + bool is_arg(string_range) const { + return false; + } +}; + +struct arg_parser { + arg_parser() {} + + 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_optional(A &&...args) { + arg_argument *p = new arg_optional{std::forward(args)...}; + return static_cast(*p_opts.emplace_back(p)); + } + + template + arg_positional &add_positional(A &&...args) { + arg_argument *p = new arg_positional{std::forward(args)...}; + return static_cast(*p_opts.emplace_back(p)); + } + + template + arg_category &add_category(A &&...args) { + arg_argument *p = new arg_category{std::forward(args)...}; + return static_cast(*p_opts.emplace_back(p)); + } + +private: + template + void parse_long(string_range arg, R &args) { + string_range val = find(arg, '='); + bool has_val = !val.empty(), arg_val = false; + if (has_val) { + arg = arg.slice(0, arg.size() - val.size()); + val.pop_front(); + } + + auto &desc = static_cast(find_arg(arg)); + + args.pop_front(); + auto needs = desc.needs_value(); + if (needs == arg_value::NONE) { + if (has_val) { + throw arg_error{format( + appender(), "argument '%s' takes no value", + desc.p_lname + ).get()}; + } + return; + } + if (!has_val) { + if (args.empty()) { + if (needs == arg_value::REQUIRED) { + throw arg_error{format( + appender(), "argument '%s' needs a value", + desc.p_lname + ).get()}; + } + return; + } + string_range tval = args.front(); + if ((needs != arg_value::OPTIONAL) || !find_arg_ptr(tval)) { + val = tval; + has_val = arg_val = true; + } + } + if (has_val) { + desc.set_value(val); + if (arg_val) { + args.pop_front(); + } + } + } + + template + void parse_short(string_range arg, R &args) { + string_range val; + bool has_val = (arg.size() > 2), arg_val = false; + if (has_val) { + val = arg.slice(2); + arg = arg.slice(0, 2); + } + + auto &desc = static_cast(find_arg(arg)); + + args.pop_front(); + auto needs = desc.needs_value(); + if (needs == arg_value::NONE) { + if (has_val) { + throw arg_error{format( + appender(), "argument '-%c' takes no value", + desc.p_sname + ).get()}; + } + return; + } + if (!has_val) { + if (args.empty()) { + if (needs == arg_value::REQUIRED) { + throw arg_error{format( + appender(), "argument '-%c' needs a value", + desc.p_sname + ).get()}; + } + return; + } + string_range tval = args.front(); + if ((needs != arg_value::OPTIONAL) || !find_arg_ptr(tval)) { + val = tval; + has_val = arg_val = true; + } + } + if (has_val) { + desc.set_value(val); + if (arg_val) { + args.pop_front(); + } + } + } + + void parse_pos(string_range) { + } + + arg_argument *find_arg_ptr(string_range name) { + for (auto &p: p_opts) { + if (p->is_arg(name)) { + return &*p; + } + } + return nullptr; + } + + arg_argument &find_arg(string_range name) { + auto p = find_arg_ptr(name); + if (p) { + return *p; + } + throw arg_error{format( + appender(), "unknown argument '%s'", name + ).get()}; + } + + std::vector> p_opts; +}; + +/** @} */ + +} /* namespace ostd */ + +#endif + +/** @} */