diff --git a/libs/yocto/yocto_commonio.h b/libs/yocto/yocto_commonio.h index 4acdc03df..29d49f4cb 100644 --- a/libs/yocto/yocto_commonio.h +++ b/libs/yocto/yocto_commonio.h @@ -41,11 +41,12 @@ #include #include #include +#include #include #include #include #include -#include +#include #include // ----------------------------------------------------------------------------- @@ -54,8 +55,9 @@ namespace yocto { // using directives +using std::function; using std::string; -using std::unordered_map; +using std::unordered_set; using std::vector; using namespace std::string_literals; @@ -68,7 +70,7 @@ namespace yocto { // Print a message to the console inline void print_info(const string& msg); -// Prints a messgae to the console and exit with an error. +// Prints a message to the console and exit with an error. inline void print_fatal(const string& msg); // Timer that prints as scope end. Create with `print_timed` and print with @@ -109,28 +111,24 @@ inline string get_usage(const cli_state& cli); // gets whether help was invoked inline bool get_help(const cli_state& cli); -// Parse an int, float, string, and bool option or positional argument. -// Options's names starts with "--" or "-", otherwise they are arguments. -// The library support using many names for the same option/argument -// separate by commas. Boolean flags are indicated with a pair of names -// "--name/--no-name", so that we have both options available. -inline void add_option(cli_state& cli, const string& name, string& value, - const string& usage, bool req = false); -inline void add_option(cli_state& cli, const string& name, int& value, - const string& usage, bool req = false); -inline void add_option(cli_state& cli, const string& name, float& value, - const string& usage, bool req = false); -inline void add_option(cli_state& cli, const string& name, bool& value, +// Parses an optional or positional argument. Optional arguments' names start +// with "--" or "-", otherwise they are arguments. Supports strings, numbers, +// boolean flags and enums. +// Many names, separated by commas, can be used for each argument. +// Boolean flags are indicated with a pair of names "--name/--no-name", so that +// both options are explicitly specified. +template +inline void add_option(cli_state& cli, const string& name, T& value, const string& usage, bool req = false); -// Parse an enum -inline void add_option(cli_state& cli, const string& name, int& value, - const string& usage, const vector& choices, bool req = false); -template >> +// Parses an optional or positional argument where values can only be within a +// set of choices. Supports strings, integers and enums. +template inline void add_option(cli_state& cli, const string& name, T& value, const string& usage, const vector& choices, bool req = false); -// Parse all arguments left on the command line. -inline void add_option(cli_state& cli, const string& name, - vector& value, const string& usage, bool req = false); +// Parse all arguments left on the command line. Can only be used as argument. +template +inline void add_option(cli_state& cli, const string& name, vector& value, + const string& usage, bool req = false); } // namespace yocto @@ -461,27 +459,19 @@ inline bool save_binary( namespace yocto { // Command line parser data. All data should be considered private. -enum struct cli_type { - // clang-format off - string_, int_, float_, bool_, flag_, string_vector_, enum_ - // clang-format on -}; -struct cmdline_option { - string name = ""; - string usage = ""; - cli_type type = cli_type::string_; - void* value = nullptr; - bool req = false; - bool set = false; - vector choices = {}; +struct cli_option { + string name = ""; + bool req = false; + int nargs = 0; + function&)> parse_and_set = {}; }; struct cli_state { - string name = ""; - string usage = ""; - vector options = {}; - string usage_options = ""; - string usage_arguments = ""; - bool help = false; + string name = ""; + string usage = ""; + vector options = {}; + string usage_options = ""; + string usage_arguments = ""; + bool help = false; }; // initialize a command line parser @@ -512,45 +502,145 @@ inline vector split_cli_names(const string& name_) { return split; } -inline void add_option(cli_state& cli, const string& name, cli_type type, - void* value, const string& usage, bool req, const vector& choices) { - static auto type_name = unordered_map{ - {cli_type::string_, ""}, - {cli_type::int_, ""}, - {cli_type::float_, ""}, - {cli_type::bool_, ""}, - {cli_type::flag_, ""}, - {cli_type::string_vector_, "<[string]>"}, - {cli_type::enum_, ""}, - }; - // help message - auto line = " " + name + " " + type_name.at(type); - while (line.size() < 32) line += " "; - line += usage; - if (!req) { - line += " ["; - switch (type) { - case cli_type::string_: line += *(string*)value; break; - case cli_type::int_: line += std::to_string(*(int*)value); break; - case cli_type::float_: line += std::to_string(*(float*)value); break; - case cli_type::bool_: line += *(bool*)value ? "true" : "false"; break; - case cli_type::flag_: line += *(bool*)value ? "true" : "false"; break; - case cli_type::enum_: line += choices.at(*(int*)value); break; - case cli_type::string_vector_: { - for (auto i = 0; i < (*(vector*)value).size(); i++) { - if (i) line += ","; - line += (*(vector*)value)[i]; - } - } break; - default: throw std::runtime_error("unknown type"); +template +struct cli_is_vector : std::false_type {}; +template +struct cli_is_vector> : std::true_type {}; +template +struct cli_is_vector> : std::true_type {}; +template +inline constexpr bool cli_is_vector_v = cli_is_vector::value; + +template +inline string cli_type_name() { + if constexpr (std::is_same_v) return ""; + if constexpr (std::is_same_v) return ""; + if constexpr (std::is_integral_v) return ""; + if constexpr (std::is_floating_point_v) return ""; + if constexpr (std::is_enum_v) return ""; + if constexpr (cli_is_vector_v) + return "<[" + cli_type_name() + "]>"; + return ""; +} + +template +inline string cli_to_string(const T& value, const vector& choices) { + if constexpr (std::is_same_v) return value; + if constexpr (std::is_same_v) return value ? "true" : "false"; + if constexpr (std::is_integral_v) + return choices.empty() ? std::to_string(value) : choices.at(value); + if constexpr (std::is_floating_point_v) return std::to_string(value); + if constexpr (std::is_enum_v) + return choices.empty() ? std::to_string((int)value) + : choices.at((int)value); + if constexpr (cli_is_vector_v) { + auto def = string{"["}; + for (auto i = 0; i < value.size(); i++) + def += (i ? "," : "") + cli_to_string(value[i], choices); + return def; + } + throw std::invalid_argument{"unsupported type"}; +} + +template +inline int cli_nargs() { + if constexpr (std::is_same_v) return 1; + if constexpr (std::is_same_v) return 0; + if constexpr (std::is_integral_v) return 1; + if constexpr (std::is_floating_point_v) return 1; + if constexpr (std::is_enum_v) return 1; + if constexpr (cli_is_vector_v) return -1; + throw std::invalid_argument{"unsupported type"}; +} + +template +inline bool parse_cli_value( + const vector& args, T& value, const vector& choices) { + if (!choices.empty()) { + for (auto& arg : args) { + if (std::find(choices.begin(), choices.end(), arg) == choices.end()) + return false; + } + } + if constexpr (std::is_same_v) { + if (args.size() != 1) return false; + value = args[0]; + return true; + } else if constexpr (std::is_integral_v) { + if (args.size() != 1) return false; + if (choices.empty()) { + auto end = (char*)nullptr; + value = (int)strtol(args[0].c_str(), &end, 10); + return end != nullptr; + } else { + value = std::find(choices.begin(), choices.end(), args[0]) - + choices.begin(); + return true; } - line += "]"; + } else if constexpr (std::is_same_v) { + if (args.size() != 1) return false; + auto end = (char*)nullptr; + value = strtof(args[0].c_str(), &end); + return end != nullptr; + } else if constexpr (std::is_same_v) { + if (args.size() != 1) return false; + auto end = (char*)nullptr; + value = strtod(args[0].c_str(), &end); + return end != nullptr; + } else if constexpr (std::is_same_v) { + if (args.size() != 1) return false; + if (args[0] == "true" || args[0] == "1") { + value = true; + return true; + } else if (args[0] == "false" || args[0] == "0") { + value = false; + return true; + } else { + return false; + } + } else if constexpr (std::is_enum_v) { + auto ivalue = 0; + if (!parse_cli_value(args, ivalue, choices)) return false; + value = (T)ivalue; + return true; + } else if constexpr (std::is_same_v>) { + value = args; + return true; + } else if constexpr (cli_is_vector_v) { + value.clear(); + for (auto& arg : args) + if (!parse_cli_value({arg}, value.emplace_back())) return false; + return true; } else { - line += " [required]"; + throw std::invalid_argument{"unsupported type"}; } - line += "\n"; - if (type == cli_type::enum_ && !choices.empty()) { - line += " with : "; +} + +template +inline void add_cli_option(cli_state& cli, const string& name, T& value, + const string& usage, bool req, const vector& choices) { + // check for errors + auto used = unordered_set{}; + for (auto& option : cli.options) { + if (option.name.empty()) throw std::runtime_error("name cannot be empty"); + auto names = split_cli_names(option.name); + if (names.empty()) throw std::runtime_error("name cannot be empty"); + for (auto& name : names) { + if (used.find(name) != used.end()) + throw std::runtime_error("option name " + name + " already in use"); + used.insert(name); + if ((name[0] == '-') != (option.name[0] == '-')) + throw std::runtime_error("inconsistent option type for " + name); + } + } + + // help message + auto line = " " + name + " " + cli_type_name(); + while (line.size() < 32) line += " "; + line += usage; + line += !req ? " [" + cli_to_string(value, choices) + "]\n" : " [required]\n"; + if (!choices.empty()) { + line += " with choices: "; auto len = 16; for (auto& choice : choices) { if (len + choice.size() + 2 > 78) { @@ -569,43 +659,37 @@ inline void add_option(cli_state& cli, const string& name, cli_type type, cli.usage_arguments += line; } // add option - cli.options.push_back( - cmdline_option{name, usage, type, value, req, false, choices}); + cli.options.push_back({name, req, cli_nargs(), + [&value, choices](const vector& args) -> bool { + return parse_cli_value(args, value, choices); + }}); } -inline void add_option(cli_state& cli, const string& name, string& value, - const string& usage, bool req) { - return add_option(cli, name, cli_type::string_, &value, usage, req, {}); -} -inline void add_option(cli_state& cli, const string& name, int& value, - const string& usage, bool req) { - return add_option(cli, name, cli_type::int_, &value, usage, req, {}); -} -inline void add_option(cli_state& cli, const string& name, float& value, - const string& usage, bool req) { - return add_option(cli, name, cli_type::float_, &value, usage, req, {}); -} -inline void add_option(cli_state& cli, const string& name, bool& value, - const string& usage, bool req) { - return add_option(cli, name, cli_type::flag_, &value, usage, req, {}); -} -inline void add_option(cli_state& cli, const string& name, - vector& value, const string& usage, bool req) { - return add_option( - cli, name, cli_type::string_vector_, &value, usage, req, {}); -} -inline void add_flag(cli_state& cli, const string& name, bool& value, +template +inline void add_option(cli_state& cli, const string& name, T& value, const string& usage, bool req) { - return add_option(cli, name, cli_type::flag_, &value, usage, req, {}); -} -inline void add_option(cli_state& cli, const string& name, int& value, - const string& usage, const vector& choices, bool req) { - return add_option(cli, name, cli_type::enum_, &value, usage, req, choices); + static_assert(std::is_same_v || std::is_same_v || + std::is_integral_v || std::is_floating_point_v || + std::is_enum_v, + "unsupported type"); + return add_cli_option(cli, name, value, usage, req, {}); } -template +template inline void add_option(cli_state& cli, const string& name, T& value, const string& usage, const vector& choices, bool req) { - return add_option(cli, name, (int&)value, usage, choices, req); + static_assert( + std::is_same_v || std::is_integral_v || std::is_enum_v, + "unsupported type"); + return add_cli_option(cli, name, value, usage, req, choices); +} +template +inline void add_option(cli_state& cli, const string& name, vector& value, + const string& usage, bool req) { + static_assert(std::is_same_v || std::is_same_v || + std::is_integral_v || std::is_floating_point_v || + std::is_enum_v, + "unsupported type"); + return add_cli_option(cli, name, value, usage, req, {}); } inline bool get_help(const cli_state& cli) { return cli.help; } @@ -622,28 +706,6 @@ inline string get_usage(const cli_state& cli) { return message; } -inline bool parse_cli_value(const string& str, int& value) { - auto end = (char*)nullptr; - value = (int)strtol(str.c_str(), &end, 10); - return end != nullptr; -} -inline bool parse_cli_value(const string& str, float& value) { - auto end = (char*)nullptr; - value = strtof(str.c_str(), &end); - return end != nullptr; -} -inline bool parse_cli_value(const string& str, bool& value) { - if (str == "true" || str == "1") { - value = true; - return true; - } else if (str == "false" || str == "0") { - value = false; - return true; - } else { - return false; - } -} - inline bool parse_cli( cli_state& cli, int argc, const char** argv, string& error) { auto cli_error = [&error](const string& message) { @@ -651,67 +713,36 @@ inline bool parse_cli( return false; }; - // check for errors - auto used = unordered_map{}; - for (auto& option : cli.options) { - if (option.name.empty()) throw std::runtime_error("name cannot be empty"); - auto names = split_cli_names(option.name); - if (names.empty()) throw std::runtime_error("name cannot be empty"); - for (auto& name : names) { - if (used.find(name) != used.end()) - throw std::runtime_error("option name " + name + " already in use"); - used[name] = 1; - if ((name[0] == '-') != (option.name[0] == '-')) - throw std::runtime_error("inconsistent option type for " + name); - } - } // prepare args auto args = vector{argv + 1, argv + argc}; // parse options for (auto& option : cli.options) { if (option.name[0] != '-') continue; + auto set = false; + auto values = vector{}; for (auto& name : split_cli_names(option.name)) { + if (std::find(args.begin(), args.end(), name) == args.end()) continue; auto pos = std::find(args.begin(), args.end(), name) - args.begin(); - if (pos >= args.size()) continue; - if (option.type == cli_type::flag_) { - *(bool*)option.value = name.find("--no-") == string::npos; - option.set = true; - args.erase(args.begin() + pos); - } else { - if (pos + 1 >= args.size()) + args.erase(args.begin() + pos); + if (option.nargs == 0) { + values = {name.find("--no-") == string::npos ? "true" : "false"}; + set = true; + } else if (option.nargs > 0) { + if (pos + option.nargs > args.size()) return cli_error("missing value for " + name); - auto value = args[pos + 1]; - args.erase(args.begin() + pos, args.begin() + pos + 2); - if (option.type == cli_type::string_) { - *(string*)option.value = value; - option.set = true; - } else if (option.type == cli_type::int_) { - if (!parse_cli_value(value, *(int*)option.value)) - return cli_error("incorrect value for " + name); - option.set = true; - } else if (option.type == cli_type::float_) { - if (!parse_cli_value(value, *(float*)option.value)) - return cli_error("incorrect value for " + name); - option.set = true; - } else if (option.type == cli_type::bool_) { - if (!parse_cli_value(value, *(bool*)option.value)) - return cli_error("incorrect value for " + name); - option.set = true; - } else if (option.type == cli_type::enum_) { - auto pos = std::find( - option.choices.begin(), option.choices.end(), value); - if (pos == option.choices.end()) - return cli_error("incorrect value for " + name); - else - *(int*)option.value = (int)(pos - option.choices.begin()); - option.set = true; - } else { - throw std::runtime_error("unsupported type"); - } + values = {args.begin() + pos, args.begin() + pos + option.nargs}; + set = true; + args.erase(args.begin() + pos, args.begin() + pos + option.nargs); + } else { + throw std::invalid_argument{"unsupported number of arguments"}; } } - if (option.req && !option.set) - return cli_error("missing value for " + option.name); + if (set) { + if (!option.parse_and_set(values)) + return cli_error("bad value for " + option.name); + } else { + if (option.req) return cli_error("missing value for " + option.name); + } } // check unknown options for (auto& arg : args) { @@ -720,33 +751,28 @@ inline bool parse_cli( // parse positional for (auto& option : cli.options) { if (option.name[0] == '-') continue; + auto set = false; + auto values = vector{}; if (args.empty()) { if (option.req) return cli_error("missing value for " + option.name); - } else if (option.type == cli_type::string_vector_) { - *(vector*)option.value = args; - option.set = true; + } else if (option.nargs < 0) { + values = args; + set = true; args.clear(); + } else if (option.nargs > 0) { + if (option.nargs > args.size()) + return cli_error("missing value for " + option.name); + values = {args.begin(), args.begin() + option.nargs}; + args.erase(args.begin(), args.begin() + option.nargs); + set = true; } else { - auto value = args.front(); - args.erase(args.begin()); - if (option.type == cli_type::string_) { - *(string*)option.value = value; - option.set = true; - } else if (option.type == cli_type::int_) { - if (!parse_cli_value(value, *(int*)option.value)) - return cli_error("incorrect value for " + option.name); - option.set = true; - } else if (option.type == cli_type::float_) { - if (!parse_cli_value(value, *(float*)option.value)) - return cli_error("incorrect value for " + option.name); - option.set = true; - } else if (option.type == cli_type::bool_) { - if (!parse_cli_value(value, *(bool*)option.value)) - return cli_error("incorrect value for " + option.name); - option.set = true; - } else { - throw std::runtime_error("unsupported type"); - } + throw std::invalid_argument{"unsupported number of arguments"}; + } + if (set) { + if (!option.parse_and_set(values)) + return cli_error("bad value for " + option.name); + } else { + if (option.req) return cli_error("missing value for " + option.name); } } // check remaining