| /** @file |
| |
| Powerful and easy-to-use command line parsing for ATS |
| |
| @section license License |
| |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| #include "tscore/ArgParser.h" |
| #include "tscore/ink_file.h" |
| #include "tscore/Version.h" |
| |
| #include <iostream> |
| #include <set> |
| #include <sstream> |
| #include <utility> |
| #include <sysexits.h> |
| |
| std::string global_usage; |
| std::string description; |
| std::string parser_program_name; |
| std::string default_command; |
| |
| // by default return EX_USAGE(64) when usage is called. |
| // if -h or --help is called specifically, return 0 |
| int usage_return_code = EX_USAGE; |
| |
| namespace ts |
| { |
| bool ArgParser::_test_mode = false; |
| |
| ArgParser::ArgParser() {} |
| |
| ArgParser::ArgParser(std::string const &name, std::string const &description, std::string const &envvar, unsigned arg_num, |
| Function const &f) |
| { |
| // initialize _top_level_command according to the provided message |
| _top_level_command = ArgParser::Command(name, description, envvar, arg_num, f); |
| } |
| |
| ArgParser::~ArgParser() {} |
| |
| // add new options with args |
| ArgParser::Command & |
| ArgParser::add_option(std::string const &long_option, std::string const &short_option, std::string const &description, |
| std::string const &envvar, unsigned arg_num, std::string const &default_value, std::string const &key) |
| { |
| return _top_level_command.add_option(long_option, short_option, description, envvar, arg_num, default_value, key); |
| } |
| |
| // Create a mutually exclusive group |
| ArgParser::Command & |
| ArgParser::add_mutex_group(std::string const &group_name, bool required, std::string const &description) |
| { |
| return _top_level_command.add_mutex_group(group_name, required, description); |
| } |
| |
| // Add an option to a mutually exclusive group |
| ArgParser::Command & |
| ArgParser::add_option_to_group(std::string const &group_name, std::string const &long_option, std::string const &short_option, |
| std::string const &description, std::string const &envvar, unsigned arg_num, |
| std::string const &default_value, std::string const &key) |
| { |
| return _top_level_command.add_option_to_group(group_name, long_option, short_option, description, envvar, arg_num, default_value, |
| key); |
| } |
| |
| // add sub-command with only function |
| ArgParser::Command & |
| ArgParser::add_command(std::string const &cmd_name, std::string const &cmd_description, Function const &f, std::string const &key) |
| { |
| return _top_level_command.add_command(cmd_name, cmd_description, f, key); |
| } |
| |
| // add sub-command without args and function |
| ArgParser::Command & |
| ArgParser::add_command(std::string const &cmd_name, std::string const &cmd_description, std::string const &cmd_envvar, |
| unsigned cmd_arg_num, Function const &f, std::string const &key) |
| { |
| return _top_level_command.add_command(cmd_name, cmd_description, cmd_envvar, cmd_arg_num, f, key); |
| } |
| |
| void |
| ArgParser::add_global_usage(std::string const &usage) |
| { |
| global_usage = usage; |
| } |
| |
| void |
| ArgParser::help_message(std::string_view err) const |
| { |
| _top_level_command.help_message(err); |
| } |
| |
| // a graceful way to output help message |
| void |
| ArgParser::Command::help_message(std::string_view err) const |
| { |
| if (!description.empty()) { |
| std::cout << description << '\n'; |
| } |
| |
| if (!err.empty()) { |
| std::cout << "Error: " << err << std::endl; |
| } |
| // output global usage |
| if (global_usage.size() > 0) { |
| std::cout << "\nUsage: " + global_usage << std::endl; |
| } |
| |
| // output subcommands |
| std::cout << "\nCommands ---------------------- Description -----------------------" << std::endl; |
| std::string prefix = ""; |
| output_command(std::cout, prefix); |
| // output options |
| if (_option_list.size() > 0) { |
| std::cout << "\nOptions ======================= Default ===== Description =============" << std::endl; |
| output_option(); |
| } |
| // output example usages |
| if (!_example_usages.empty()) { |
| std::cout << "\nExample Usage:" << std::endl; |
| for (const auto &example : _example_usages) { |
| std::cout << " " << example << std::endl; |
| } |
| } |
| // standard return code |
| ArgParser::do_exit(usage_return_code); |
| } |
| |
| void |
| ArgParser::Command::version_message() const |
| { |
| // unified version message of ATS |
| AppVersionInfo::setup_version(_name.c_str()); |
| AppVersionInfo::print_version(); |
| ArgParser::do_exit(0); |
| } |
| |
| void |
| ArgParser::set_default_command(std::string const &cmd) |
| { |
| if (default_command.empty()) { |
| if (_top_level_command._subcommand_list.find(cmd) == _top_level_command._subcommand_list.end()) { |
| std::cerr << "Error: Default command " << cmd << "not found" << std::endl; |
| ArgParser::do_exit(1); |
| } |
| default_command = cmd; |
| } else if (cmd != default_command) { |
| std::cerr << "Error: Default command " << default_command << "already existed" << std::endl; |
| ArgParser::do_exit(1); |
| } |
| } |
| |
| // Top level call of parsing |
| Arguments |
| ArgParser::parse(const char **argv) |
| { |
| // deal with argv first |
| int size = 0; |
| _argv.clear(); |
| while (argv[size]) { |
| _argv.push_back(argv[size]); |
| size++; |
| } |
| if (size == 0) { |
| std::cout << "Error: invalid argv provided" << std::endl; |
| ArgParser::do_exit(1); |
| } |
| // the name of the program only |
| _argv[0] = _argv[0].substr(_argv[0].find_last_of('/') + 1); |
| _top_level_command._name = _argv[0]; |
| _top_level_command._key = _argv[0]; |
| parser_program_name = _argv[0]; |
| Arguments ret; // the parsed arg object to return |
| AP_StrVec args = _argv; |
| // call the recursive parse method in Command |
| if (!_top_level_command.parse(ret, args)) { |
| // deal with default command |
| if (!default_command.empty()) { |
| args = _argv; |
| args.insert(args.begin() + 1, default_command); |
| _top_level_command.parse(ret, args); |
| } |
| }; |
| // if there is anything left, then output usage |
| if (!args.empty()) { |
| std::string msg = "Unknown command, option or args:"; |
| for (const auto &it : args) { |
| msg = msg + " '" + it + "'"; |
| } |
| // find the correct level to output help message |
| ArgParser::Command *command = &_top_level_command; |
| for (unsigned i = 1; i < _argv.size(); i++) { |
| auto it = command->_subcommand_list.find(_argv[i]); |
| if (it == command->_subcommand_list.end()) { |
| break; |
| } |
| command = &it->second; |
| } |
| command->help_message(msg); |
| } |
| return ret; |
| } |
| |
| ArgParser::Command & |
| ArgParser::require_commands() |
| { |
| return _top_level_command.require_commands(); |
| } |
| |
| void |
| ArgParser::set_error(std::string e) |
| { |
| _error_msg = std::move(e); |
| } |
| |
| std::string |
| ArgParser::get_error() const |
| { |
| return _error_msg; |
| } |
| |
| void |
| ArgParser::add_description(std::string const &descr) |
| { |
| description = descr; |
| } |
| //=========================== Command class ================================ |
| ArgParser::Command::Command() {} |
| |
| ArgParser::Command::~Command() {} |
| |
| ArgParser::Command::Command(std::string const &name, std::string const &description, std::string const &envvar, unsigned arg_num, |
| Function const &f, std::string const &key) |
| : _name(name), _description(description), _arg_num(arg_num), _envvar(envvar), _f(f), _key(key) |
| { |
| } |
| |
| // check if this is a valid option before adding |
| void |
| ArgParser::Command::check_option(std::string const &long_option, std::string const &short_option, |
| std::string const & /* key ATS_UNUSED */) const |
| { |
| if (long_option.size() < 3 || long_option[0] != '-' || long_option[1] != '-') { |
| // invalid name |
| std::cerr << "Error: invalid long option added: '" + long_option + "'" << std::endl; |
| ArgParser::do_exit(1); |
| } |
| if (short_option.size() > 2 || (short_option.size() > 0 && short_option[0] != '-')) { |
| // invalid short option |
| std::cerr << "Error: invalid short option added: '" + short_option + "'" << std::endl; |
| ArgParser::do_exit(1); |
| } |
| // find if existing in option list |
| if (_option_list.find(long_option) != _option_list.end()) { |
| std::cerr << "Error: long option '" + long_option + "' already existed" << std::endl; |
| ArgParser::do_exit(1); |
| } else if (_option_map.find(short_option) != _option_map.end()) { |
| std::cerr << "Error: short option '" + short_option + "' already existed" << std::endl; |
| ArgParser::do_exit(1); |
| } |
| } |
| |
| // check if this is a valid command before adding |
| void |
| ArgParser::Command::check_command(std::string const &name, std::string const & /* key ATS_UNUSED */) const |
| { |
| if (name.empty()) { |
| // invalid name |
| std::cerr << "Error: empty command cannot be added" << std::endl; |
| ArgParser::do_exit(1); |
| } |
| // find if existing in subcommand list |
| if (_subcommand_list.find(name) != _subcommand_list.end()) { |
| std::cerr << "Error: command already exists: '" + name + "'" << std::endl; |
| ArgParser::do_exit(1); |
| } |
| } |
| |
| // add new options with args |
| ArgParser::Command & |
| ArgParser::Command::add_option(std::string const &long_option, std::string const &short_option, std::string const &description, |
| std::string const &envvar, unsigned arg_num, std::string const &default_value, |
| std::string const &key) |
| { |
| std::string lookup_key = key.empty() ? long_option.substr(2) : key; |
| check_option(long_option, short_option, lookup_key); |
| _option_list[long_option] = {long_option, short_option == "-" ? "" : short_option, description, envvar, arg_num, default_value, |
| lookup_key}; |
| if (short_option != "-" && !short_option.empty()) { |
| _option_map[short_option] = long_option; |
| } |
| _last_added_option = long_option; // track for with_required() chaining |
| return *this; |
| } |
| |
| // Create a mutually exclusive group |
| ArgParser::Command & |
| ArgParser::Command::add_mutex_group(std::string const &group_name, bool required, std::string const &description) |
| { |
| if (group_name.empty()) { |
| std::cerr << "Error: Mutex group name cannot be empty" << std::endl; |
| ArgParser::do_exit(1); |
| } |
| |
| if (_mutex_groups.find(group_name) != _mutex_groups.end()) { |
| std::cerr << "Error: Mutex group '" << group_name << "' already exists" << std::endl; |
| ArgParser::do_exit(1); |
| } |
| _mutex_groups.emplace(group_name, MutexGroup(group_name, required, description)); |
| return *this; |
| } |
| |
| // Add an option to a mutually exclusive group |
| ArgParser::Command & |
| ArgParser::Command::add_option_to_group(std::string const &group_name, std::string const &long_option, |
| std::string const &short_option, std::string const &description, std::string const &envvar, |
| unsigned arg_num, std::string const &default_value, std::string const &key) |
| { |
| if (group_name.empty()) { |
| std::cerr << "Error: Mutex group name cannot be empty" << std::endl; |
| ArgParser::do_exit(1); |
| } |
| |
| auto it_mutex_group = _mutex_groups.find(group_name); |
| if (it_mutex_group == _mutex_groups.end()) { |
| std::cerr << "Error: Mutex group '" << group_name << "' not found" << std::endl; |
| ArgParser::do_exit(1); |
| } |
| |
| // Add the option normally (this also sets _last_added_option) |
| add_option(long_option, short_option, description, envvar, arg_num, default_value, key); |
| |
| // Track this option in the mutex group |
| it_mutex_group->second.options.push_back(long_option); |
| _option_to_group[long_option] = group_name; |
| |
| return *this; |
| } |
| |
| // add sub-command with only function |
| ArgParser::Command & |
| ArgParser::Command::add_command(std::string const &cmd_name, std::string const &cmd_description, Function const &f, |
| std::string const &key) |
| { |
| std::string lookup_key = key.empty() ? cmd_name : key; |
| check_command(cmd_name, lookup_key); |
| _subcommand_list[cmd_name] = ArgParser::Command(cmd_name, cmd_description, "", 0, f, lookup_key); |
| return _subcommand_list[cmd_name]; |
| } |
| |
| // add sub-command without args and function |
| ArgParser::Command & |
| ArgParser::Command::add_command(std::string const &cmd_name, std::string const &cmd_description, std::string const &cmd_envvar, |
| unsigned cmd_arg_num, Function const &f, std::string const &key) |
| { |
| std::string lookup_key = key.empty() ? cmd_name : key; |
| check_command(cmd_name, lookup_key); |
| _subcommand_list[cmd_name] = ArgParser::Command(cmd_name, cmd_description, cmd_envvar, cmd_arg_num, f, lookup_key); |
| return _subcommand_list[cmd_name]; |
| } |
| |
| ArgParser::Command & |
| ArgParser::Command::add_example_usage(std::string const &usage) |
| { |
| _example_usages.push_back(usage); |
| return *this; |
| } |
| |
| // method used by help_message() |
| void |
| ArgParser::Command::output_command(std::ostream &out, std::string const &prefix) const |
| { |
| if (_name != parser_program_name) { |
| // a nicely formatted way to output command usage |
| std::string msg = prefix + _name; |
| // nicely formatted output |
| if (!_description.empty()) { |
| if (INDENT_ONE - static_cast<int>(msg.size()) < 0) { |
| // if the command msg is too long |
| std::cout << msg << "\n" << std::string(INDENT_ONE, ' ') << _description << std::endl; |
| } else { |
| std::cout << msg << std::string(INDENT_ONE - msg.size(), ' ') << _description << std::endl; |
| } |
| } |
| } |
| // recursive call |
| for (const auto &it : _subcommand_list) { |
| it.second.output_command(out, " " + prefix); |
| } |
| } |
| |
| // a nicely formatted way to output option message for help. |
| void |
| ArgParser::Command::output_option() const |
| { |
| // Helper method to build argument message |
| auto arg_msg_builder = [](unsigned num) -> std::string { |
| if (num == 1) { |
| return {" <arg>"}; |
| } else if (num == MORE_THAN_ZERO_ARG_N) { |
| return {" [<arg> ...]"}; |
| } else if (num == MORE_THAN_ONE_ARG_N) { |
| return {" <arg> ..."}; |
| } else { |
| return " <arg1> ... <arg" + std::to_string(num) + ">"; |
| } |
| }; |
| |
| // First, output regular options (excluding those in mutex groups) |
| for (const auto &it : _option_list) { |
| // Skip if this option is in a mutex group (it will be displayed in the mutex group) |
| if (_option_to_group.find(it.first) != _option_to_group.end()) { |
| continue; |
| } |
| |
| std::string msg; |
| if (!it.second.short_option.empty()) { |
| msg = it.second.short_option + ", "; |
| } |
| |
| msg += it.first; |
| if (it.second.arg_num != 0) { |
| msg += arg_msg_builder(it.second.arg_num); |
| } |
| |
| if (!it.second.default_value.empty()) { |
| if (INDENT_ONE - static_cast<int>(msg.size()) < 0) { |
| msg = msg + "\n" + std::string(INDENT_ONE, ' ') + it.second.default_value; |
| } else { |
| msg = msg + std::string(INDENT_ONE - msg.size(), ' ') + it.second.default_value; |
| } |
| } |
| // Build description with dependency info if applicable |
| std::string desc = it.second.description; |
| auto dep_it = _option_dependencies.find(it.first); |
| if (dep_it != _option_dependencies.end() && !dep_it->second.empty()) { |
| if (!desc.empty()) { |
| desc += " "; |
| } |
| desc += "(requires"; |
| for (size_t i = 0; i < dep_it->second.size(); ++i) { |
| desc += " " + dep_it->second[i]; |
| if (i < dep_it->second.size() - 1) { |
| desc += ","; |
| } |
| } |
| desc += ")"; |
| } |
| |
| if (!desc.empty()) { |
| if (INDENT_TWO - static_cast<int>(msg.size()) < 0) { |
| std::cout << msg << "\n" << std::string(INDENT_TWO, ' ') << desc << std::endl; |
| } else { |
| std::cout << msg << std::string(INDENT_TWO - msg.size(), ' ') << desc << std::endl; |
| } |
| } |
| } |
| |
| // Then output mutually exclusive groups |
| for (const auto &[group_name, group] : _mutex_groups) { |
| std::cout << "\nGroup (" << group_name; |
| if (group.required) { |
| std::cout << ", required"; |
| } |
| std::cout << ")"; |
| if (!group.description.empty()) { |
| std::cout << " - " << group.description; |
| } |
| std::cout << std::endl; |
| |
| for (const auto &option_name : group.options) { |
| auto const it = _option_list.find(option_name); |
| if (it != _option_list.end()) { |
| std::string msg{" "}; // Indent group options |
| if (!it->second.short_option.empty()) { |
| msg += it->second.short_option + ", "; |
| } |
| |
| msg += it->first; |
| if (it->second.arg_num != 0) { |
| msg += arg_msg_builder(it->second.arg_num); |
| } |
| |
| if (!it->second.default_value.empty()) { |
| if (INDENT_ONE - static_cast<int>(msg.size()) < 0) { |
| msg = msg + "\n" + std::string(INDENT_ONE, ' ') + it->second.default_value; |
| } else { |
| msg = msg + std::string(INDENT_ONE - msg.size(), ' ') + it->second.default_value; |
| } |
| } |
| |
| if (!it->second.description.empty()) { |
| if (INDENT_TWO - static_cast<int>(msg.size()) < 0) { |
| std::cout << msg << "\n" << std::string(INDENT_TWO, ' ') << it->second.description << std::endl; |
| } else { |
| std::cout << msg << std::string(INDENT_TWO - msg.size(), ' ') << it->second.description << std::endl; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // helper method to handle the arguments and put them nicely in arguments |
| // can be switched to ts::errata |
| static std::string |
| handle_args(Arguments &ret, AP_StrVec &args, std::string const &name, unsigned arg_num, unsigned &index) |
| { |
| ArgumentData data; |
| ret.append(name, data); |
| // handle the args |
| if (arg_num == MORE_THAN_ZERO_ARG_N || arg_num == MORE_THAN_ONE_ARG_N) { |
| // infinite arguments |
| if (arg_num == MORE_THAN_ONE_ARG_N && args.size() <= index + 1) { |
| return "at least one argument expected by " + name; |
| } |
| for (unsigned j = index + 1; j < args.size(); j++) { |
| ret.append_arg(name, args[j]); |
| } |
| args.erase(args.begin() + index, args.end()); |
| return ""; |
| } |
| // finite number of argument handling |
| for (unsigned j = 0; j < arg_num; j++) { |
| if (args.size() < index + j + 2 || args[index + j + 1].empty()) { |
| return std::to_string(arg_num) + " argument(s) expected by " + name; |
| } |
| ret.append_arg(name, args[index + j + 1]); |
| } |
| // erase the used arguments and append the data to the return structure |
| args.erase(args.begin() + index, args.begin() + index + arg_num + 1); |
| index -= 1; |
| return ""; |
| } |
| |
| // Validate mutually exclusive groups |
| void |
| ArgParser::Command::validate_mutex_groups(Arguments &ret) const |
| { |
| // Check each mutex group |
| for (const auto &[group_name, group] : _mutex_groups) { |
| std::vector<std::string> used_options; |
| |
| // Find which options from this group were used |
| for (const auto &option_name : group.options) { |
| auto it = _option_list.find(option_name); |
| if (it != _option_list.end()) { |
| // Check if this option was called |
| if (ret.get(it->second.key)) { |
| used_options.push_back(option_name); |
| } |
| } |
| } |
| |
| // Validate: only one option from the group can be used |
| if (used_options.size() > 1) { |
| std::string error_msg = "Error: Options in mutex group '" + group_name + "' are mutually exclusive. Used: "; |
| for (size_t i = 0; i < used_options.size(); ++i) { |
| if (i > 0) { |
| error_msg += ", "; |
| } |
| error_msg += used_options[i]; |
| } |
| help_message(error_msg); |
| } |
| |
| // Validate: if group is required, at least one option must be used |
| if (group.required && used_options.empty()) { |
| std::string error_msg = "Error: One option from required mutex group '" + group_name + "' must be specified. Options: "; |
| for (size_t i = 0; i < group.options.size(); ++i) { |
| if (i > 0) { |
| error_msg += ", "; |
| } |
| error_msg += group.options[i]; |
| } |
| help_message(error_msg); |
| } |
| } |
| } |
| |
| // Specify that the last added option requires another option |
| ArgParser::Command & |
| ArgParser::Command::with_required(std::string const &required_option) |
| { |
| if (_last_added_option.empty()) { |
| std::cerr << "Error: with_required() must be called after add_option()" << std::endl; |
| ArgParser::do_exit(1); |
| } |
| |
| // Validate that required option exists |
| if (_option_list.find(required_option) == _option_list.end()) { |
| std::cerr << "Error: Required option '" << required_option << "' not found" << std::endl; |
| ArgParser::do_exit(1); |
| } |
| |
| _option_dependencies[_last_added_option].push_back(required_option); |
| |
| return *this; |
| } |
| |
| // Validate option dependencies |
| void |
| ArgParser::Command::validate_dependencies(Arguments &ret) const |
| { |
| for (const auto &[dependent, required_list] : _option_dependencies) { |
| // Get the key for the dependent option |
| auto it = _option_list.find(dependent); |
| if (it == _option_list.end()) { |
| continue; |
| } |
| |
| const std::string &dep_key = it->second.key; |
| |
| // Check if dependent option was used |
| if (ret.get(dep_key)) { |
| // Dependent option was used, check all required options |
| for (const auto &required : required_list) { |
| auto req_it = _option_list.find(required); |
| if (req_it == _option_list.end()) { |
| continue; |
| } |
| |
| const std::string &req_key = req_it->second.key; |
| |
| if (!ret.get(req_key)) { |
| std::string error_msg = "Option '" + dependent + "' requires '" + required + "' to be specified"; |
| help_message(error_msg); // exit with status code 64 (EX_USAGE - command line usage error) |
| } |
| } |
| } |
| } |
| } |
| |
| // Append the args of option to parsed data. Return true if there is any option called |
| void |
| ArgParser::Command::append_option_data(Arguments &ret, AP_StrVec &args, int index) |
| { |
| std::map<std::string, unsigned> check_map; |
| for (unsigned i = index; i < args.size(); i++) { |
| // find matches of the arg |
| if (args[i][0] == '-' && args[i][1] == '-' && args[i].find('=') != std::string::npos) { |
| // deal with --args= |
| std::string option_name = args[i].substr(0, args[i].find_first_of('=')); |
| std::string value = args[i].substr(args[i].find_last_of('=') + 1); |
| if (value.empty()) { |
| help_message("missing argument for '" + option_name + "'"); |
| } |
| auto it = _option_list.find(option_name); |
| if (it != _option_list.end()) { |
| ArgParser::Option cur_option = it->second; |
| // handle environment variable |
| if (!cur_option.envvar.empty()) { |
| const char *const env = getenv(cur_option.envvar.c_str()); |
| ret.set_env(cur_option.key, nullptr != env ? env : ""); |
| } |
| ret.append_arg(cur_option.key, value); |
| check_map[cur_option.long_option] += 1; |
| args.erase(args.begin() + i); |
| i -= 1; |
| } |
| } else { |
| // output version message |
| if ((args[i] == "--version" || args[i] == "-V") && _option_list.find("--version") != _option_list.end()) { |
| version_message(); |
| } |
| // output help message |
| if ((args[i] == "--help" || args[i] == "-h") && _option_list.find("--help") != _option_list.end()) { |
| ArgParser::Command *command = this; |
| // find the correct level to output help message |
| for (unsigned i = 1; i < args.size(); i++) { |
| auto it = command->_subcommand_list.find(args[i]); |
| if (it == command->_subcommand_list.end()) { |
| break; |
| } |
| command = &it->second; |
| } |
| usage_return_code = 0; |
| command->help_message(); |
| } |
| // deal with normal --arg val1 val2 ... |
| auto long_it = _option_list.find(args[i]); |
| auto short_it = _option_map.find(args[i]); |
| // long option match or short option match |
| if (long_it != _option_list.end() || short_it != _option_map.end()) { |
| ArgParser::Option cur_option; |
| if (long_it != _option_list.end()) { |
| cur_option = long_it->second; |
| } else { |
| cur_option = _option_list.at(short_it->second); |
| } |
| // handle the arguments |
| std::string err = handle_args(ret, args, cur_option.key, cur_option.arg_num, i); |
| if (!err.empty()) { |
| help_message(err); |
| } |
| // handle environment variable |
| if (!cur_option.envvar.empty()) { |
| const char *const env = getenv(cur_option.envvar.c_str()); |
| ret.set_env(cur_option.key, nullptr != env ? env : ""); |
| } |
| } |
| } |
| } |
| // check for wrong number of arguments for --arg=... |
| for (const auto &it : check_map) { |
| unsigned num = _option_list.at(it.first).arg_num; |
| if (num != it.second && num < MORE_THAN_ONE_ARG_N) { |
| help_message(std::to_string(_option_list.at(it.first).arg_num) + " arguments expected by " + it.first); |
| } |
| } |
| } |
| |
| // Apply default values for options not explicitly set by the user. |
| // This must be called AFTER validate_dependencies() so that default values |
| // (e.g. --timeout "0") don't falsely trigger dependency checks. |
| void |
| ArgParser::Command::apply_option_defaults(Arguments &ret) const |
| { |
| for (const auto &it : _option_list) { |
| if (!it.second.default_value.empty() && ret.get(it.second.key).empty()) { |
| std::istringstream ss(it.second.default_value); |
| std::string token; |
| while (std::getline(ss, token, ' ')) { |
| ret.append_arg(it.second.key, token); |
| } |
| } |
| } |
| } |
| |
| // Main recursive logic of Parsing |
| bool |
| ArgParser::Command::parse(Arguments &ret, AP_StrVec &args) |
| { |
| bool command_called = false; |
| // Only check the first remaining argument for command name to avoid |
| // treating arguments as commands (e.g., "metric match host" where "host" is an arg, not a command) |
| if (!args.empty() && _name == args[0]) { |
| command_called = true; |
| // Note: handle_args modifies its index parameter (designed for loop usage), but we |
| // discard the result. This causes unsigned underflow (0 - 1 = UINT_MAX) which is |
| // harmless since we don't use index afterward. |
| unsigned index{0}; |
| // handle the option |
| append_option_data(ret, args, index); |
| // handle the action |
| if (_f) { |
| ret._action = _f; |
| } |
| const std::string err = handle_args(ret, args, _key, _arg_num, index); |
| if (!err.empty()) { |
| help_message(err); |
| } |
| // set ENV var |
| if (!_envvar.empty()) { |
| const char *const env = getenv(_envvar.c_str()); |
| ret.set_env(_key, nullptr != env ? env : ""); |
| } |
| |
| // Validate mutually exclusive groups |
| validate_mutex_groups(ret); |
| |
| // Validate option dependencies |
| validate_dependencies(ret); |
| |
| // Apply default values after validation so that defaults don't |
| // trigger dependency checks (e.g. --timeout with default "0" |
| // should not require --monitor when not explicitly used). |
| apply_option_defaults(ret); |
| } |
| |
| if (command_called) { |
| bool flag = false; |
| // recursively call subcommand |
| for (auto &it : _subcommand_list) { |
| if (it.second.parse(ret, args)) { |
| flag = true; |
| break; |
| } |
| } |
| // check for command required |
| if (!flag && _command_required) { |
| std::ostringstream msg; |
| if (!args.empty()) { |
| msg << "No sub-command '" << args[0] << "' found for " << _name; |
| } else { |
| msg << "No sub-command found for " << _name; |
| } |
| help_message(msg.str()); |
| } |
| if (_name == parser_program_name) { |
| // if we are at the top level |
| return flag; |
| } |
| } |
| return command_called; |
| } |
| |
| ArgParser::Command & |
| ArgParser::Command::require_commands() |
| { |
| _command_required = true; |
| return *this; |
| } |
| |
| ArgParser::Command & |
| ArgParser::Command::set_default() |
| { |
| default_command = _name; |
| return *this; |
| } |
| |
| //=========================== Arguments class ================================ |
| |
| Arguments::Arguments() {} |
| Arguments::~Arguments() {} |
| |
| ArgumentData |
| Arguments::get(std::string const &name) |
| { |
| if (_data_map.find(name) != _data_map.end()) { |
| _data_map[name]._is_called = true; |
| return _data_map[name]; |
| } |
| return ArgumentData(); |
| } |
| |
| void |
| Arguments::append(std::string const &key, ArgumentData const &value) |
| { |
| // perform overwrite for now |
| _data_map[key] = value; |
| } |
| |
| void |
| Arguments::append_arg(std::string const &key, std::string const &value) |
| { |
| _data_map[key]._values.push_back(value); |
| } |
| |
| void |
| Arguments::set_env(std::string const &key, std::string const &value) |
| { |
| // perform overwrite for now |
| _data_map[key]._env_value = value; |
| } |
| |
| void |
| Arguments::show_all_configuration() const |
| { |
| for (const auto &it : _data_map) { |
| std::cout << "name: " + it.first << std::endl; |
| std::string msg; |
| msg = "args value:"; |
| for (const auto &it_data : it.second._values) { |
| msg += " " + it_data; |
| } |
| std::cout << msg << std::endl; |
| std::cout << "env value: " + it.second._env_value << std::endl << std::endl; |
| } |
| } |
| |
| // invoke the function with the args |
| void |
| Arguments::invoke() |
| { |
| if (_action) { |
| // call the std::function |
| _action(); |
| } else { |
| throw std::runtime_error("no function to invoke"); |
| } |
| } |
| |
| bool |
| Arguments::has_action() const |
| { |
| return _action != nullptr; |
| } |
| |
| //=========================== ArgumentData class ================================ |
| |
| std::string const & |
| ArgumentData::env() const noexcept |
| { |
| return _env_value; |
| } |
| |
| std::string const & |
| ArgumentData::at(unsigned index) const |
| { |
| if (index >= _values.size()) { |
| throw std::out_of_range("argument not found at index: " + std::to_string(index)); |
| } |
| return _values.at(index); |
| } |
| |
| std::string const & |
| ArgumentData::value() const noexcept |
| { |
| if (_values.empty()) { |
| // To prevent compiler warning |
| static const std::string empty = ""; |
| return empty; |
| } |
| return _values.at(0); |
| } |
| |
| size_t |
| ArgumentData::size() const noexcept |
| { |
| return _values.size(); |
| } |
| |
| bool |
| ArgumentData::empty() const noexcept |
| { |
| return _values.empty() && _env_value.empty(); |
| } |
| |
| AP_StrVec::const_iterator |
| ArgumentData::begin() const noexcept |
| { |
| return _values.begin(); |
| } |
| |
| AP_StrVec::const_iterator |
| ArgumentData::end() const noexcept |
| { |
| return _values.end(); |
| } |
| // protected method for testing |
| /*static*/ void |
| ArgParser::set_test_mode(bool test) |
| { |
| _test_mode = test; |
| } |
| |
| // protected method for testing |
| /*static*/ void |
| ArgParser::do_exit(int code) |
| { |
| if (_test_mode) { |
| throw std::runtime_error("Test mode: exit with code " + std::to_string(code)); |
| } |
| exit(code); |
| } |
| |
| } // namespace ts |