#include #include #include #include #include #include #include "argparse.hpp" #include "argparse_util.hpp" namespace argparse { /* * ArgumentParser */ ArgumentParser::ArgumentParser(std::string prog_name, std::string description_str, std::ostream& os) : description_(description_str) , formatter_(new DefaultFormatter()) , os_(os) { prog(prog_name); argument_groups_.push_back(ArgumentGroup("arguments")); } ArgumentParser& ArgumentParser::prog(std::string prog_name, bool basename_only) { if (basename_only) { prog_ = basename(prog_name); } else { prog_ = prog_name; } return *this; } ArgumentParser& ArgumentParser::version(std::string version_str) { version_ = version_str; return *this; } ArgumentParser& ArgumentParser::epilog(std::string epilog_str) { epilog_ = epilog_str; return *this; } ArgumentGroup& ArgumentParser::add_argument_group(std::string description_str) { argument_groups_.push_back(ArgumentGroup(description_str)); return argument_groups_[argument_groups_.size() - 1]; } void ArgumentParser::parse_args(int argc, const char* const* argv, int error_exit_code, int help_exit_code, int version_exit_code) { try { parse_args_throw(argc, argv); } catch (const argparse::ArgParseHelp&) { //Help requested print_help(); std::exit(help_exit_code); } catch (const argparse::ArgParseVersion&) { print_version(); std::exit(version_exit_code); } catch (const argparse::ArgParseError& e) { //Failed to parse std::cout << e.what() << "\n"; std::cout << "\n"; print_usage(); std::exit(error_exit_code); } } void ArgumentParser::parse_args_throw(int argc, const char* const* argv) { std::vector arg_strs; for (int i = 1; i < argc; ++i) { arg_strs.push_back(argv[i]); } parse_args_throw(arg_strs); } void ArgumentParser::parse_args_throw(std::vector arg_strs) { add_help_option_if_unspecified(); //Reset all the defaults for (const auto& group : argument_groups()) { for (const auto& arg : group.arguments()) { if (arg->default_set()) { arg->set_dest_to_default(); } } } //Create a look-up of expected argument strings and positional arguments std::map> str_to_option_arg; std::list> positional_args; for (const auto& group : argument_groups()) { for (const auto& arg : group.arguments()) { if (arg->positional()) { positional_args.push_back(arg); } else { for (const auto& opt : {arg->long_option(), arg->short_option()}) { if (opt.empty()) continue; auto ret = str_to_option_arg.insert(std::make_pair(opt, arg)); if (!ret.second) { //Option string already specified std::stringstream ss; ss << "Option string '" << opt << "' maps to multiple options"; throw ArgParseError(ss.str()); } } } } } std::set> specified_arguments; //Process the arguments for (size_t i = 0; i < arg_strs.size(); i++) { ShortArgInfo short_arg_info = no_space_short_arg(arg_strs[i], str_to_option_arg); std::shared_ptr arg; if (short_arg_info.is_no_space_short_arg) { //Short argument with no space between value arg = short_arg_info.arg; } else { //Full argument auto iter = str_to_option_arg.find(arg_strs[i]); if (iter != str_to_option_arg.end()) { arg = iter->second; } } if (arg) { //Start of an argument specified_arguments.insert(arg); if (arg->action() == Action::STORE_TRUE) { arg->set_dest_to_true(); } else if (arg->action() == Action::STORE_FALSE) { arg->set_dest_to_false(); } else if (arg->action() == Action::HELP) { arg->set_dest_to_true(); throw ArgParseHelp(); } else if (arg->action() == Action::VERSION) { arg->set_dest_to_true(); throw ArgParseVersion(); } else { assert(arg->action() == Action::STORE); size_t max_values_to_read = 0; size_t min_values_to_read = 0; if (arg->nargs() == '1') { max_values_to_read = 1; min_values_to_read = 1; } else if (arg->nargs() == '?') { max_values_to_read = 1; min_values_to_read = 0; } else if (arg->nargs() == '*') { max_values_to_read = std::numeric_limits::max(); min_values_to_read = 0; } else { assert (arg->nargs() == '+'); max_values_to_read = std::numeric_limits::max(); min_values_to_read = 1; } std::vector values; size_t nargs_read = 0; if (short_arg_info.is_no_space_short_arg) { //It is a short argument, we already have the first value if (!short_arg_info.value.empty()) { values.push_back(short_arg_info.value); ++nargs_read; } } for (; nargs_read < max_values_to_read; ++nargs_read) { size_t next_idx = i + 1 + nargs_read; if (next_idx >= arg_strs.size()) { break; } std::string str = arg_strs[next_idx]; if (is_argument(str, str_to_option_arg)) break; if (!arg->is_valid_value(str)) break; values.push_back(str); } if (nargs_read < min_values_to_read) { if (arg->nargs() == '1') { std::stringstream msg; msg << "Missing expected argument for " << arg_strs[i] << ""; throw ArgParseError(msg.str()); } else { std::stringstream msg; msg << "Expected at least " << min_values_to_read << " value"; if (min_values_to_read > 1) { msg << "s"; } msg << " for argument '" << arg_strs[i] << "'"; msg << " (found " << values.size() << ")"; throw ArgParseError(msg.str()); } } assert (nargs_read <= max_values_to_read); for (auto val : values) { if (!is_valid_choice(val, arg->choices())) { std::stringstream msg; msg << "Unexpected option value '" << values[0] << "' (expected one of: " << join(arg->choices(), ", "); msg << ") for " << arg->name(); throw ArgParseError(msg.str()); } } //Set the option values appropriately if (arg->nargs() == '1') { assert(nargs_read == 1); assert(values.size() == 1); try { arg->set_dest_to_value(values[0]); } catch (const ArgParseConversionError& e) { std::stringstream msg; msg << e.what() << " for " << arg->long_option(); auto short_opt = arg->short_option(); if (!short_opt.empty()) { msg << "/" << short_opt; } throw ArgParseConversionError(msg.str()); } } else if (arg->nargs() == '+' || arg->nargs() == '*') { if (arg->nargs() == '+') { assert(nargs_read >= 1); assert(values.size() >= 1); } for (auto value : values) { try { arg->add_value_to_dest(value); } catch (const ArgParseConversionError& e) { std::stringstream msg; msg << e.what() << " for " << arg->long_option(); auto short_opt = arg->short_option(); if (!short_opt.empty()) { msg << "/" << short_opt; } throw ArgParseConversionError(msg.str()); } } } else { std::stringstream msg; msg << "Unsupport nargs value '" << arg->nargs() << "'"; throw ArgParseError(msg.str()); } if (!short_arg_info.is_no_space_short_arg) { i += nargs_read; //Skip over the values (don't need to for short args) } } } else { if (positional_args.empty()) { //Unrecognized std::stringstream ss; ss << "Unexpected command-line argument '" << arg_strs[i] << "'"; throw ArgParseError(ss.str()); } else { //Positional argument auto pos_arg = positional_args.front(); positional_args.pop_front(); try { pos_arg->set_dest_to_value(arg_strs[i]); } catch (const ArgParseConversionError& e) { std::stringstream msg; msg << e.what() << " for positional argument " << pos_arg->long_option(); throw ArgParseConversionError(msg.str()); } auto value = arg_strs[i]; specified_arguments.insert(pos_arg); } } } //Missing positionals? for(const auto& remaining_positional : positional_args) { std::stringstream ss; ss << "Missing required positional argument: " << remaining_positional->long_option(); throw ArgParseError(ss.str()); } //Missing required? for (const auto& group : argument_groups()) { for (const auto& arg : group.arguments()) { if (arg->required()) { //potentially slow... if (!specified_arguments.count(arg)) { std::stringstream msg; msg << "Missing required argument: " << arg->long_option(); auto short_opt = arg->short_option(); if (!short_opt.empty()) { msg << "/" << short_opt; } throw ArgParseError(msg.str()); } } } } } void ArgumentParser::reset_destinations() { for (const auto& group : argument_groups()) { for (const auto& arg : group.arguments()) { arg->reset_dest(); } } } void ArgumentParser::print_usage() { formatter_->set_parser(this); os_ << formatter_->format_usage(); } void ArgumentParser::print_help() { formatter_->set_parser(this); os_ << formatter_->format_usage(); os_ << formatter_->format_description(); os_ << formatter_->format_arguments(); os_ << formatter_->format_epilog(); } void ArgumentParser::print_version() { formatter_->set_parser(this); os_ << formatter_->format_version(); } std::string ArgumentParser::prog() const { return prog_; } std::string ArgumentParser::version() const { return version_; } std::string ArgumentParser::description() const { return description_; } std::string ArgumentParser::epilog() const { return epilog_; } std::vector ArgumentParser::argument_groups() const { return argument_groups_; } void ArgumentParser::add_help_option_if_unspecified() { //Has a help already been specified bool found_help = false; for(auto& grp : argument_groups_) { for(auto& arg : grp.arguments()) { if(arg->action() == Action::HELP) { found_help = true; break; } } } if (!found_help) { auto& grp = argument_groups_[0]; grp.add_argument(show_help_dummy_, "--help", "-h") .help("Shows this help message") .action(Action::HELP); } } ArgumentParser::ShortArgInfo ArgumentParser::no_space_short_arg(std::string str, const std::map>& str_to_option_arg) const { ShortArgInfo short_arg_info; for(auto kv : str_to_option_arg) { if (kv.first.size() == 2) { //Is a short arg //String starts with short arg bool match = true; for (size_t i = 0; i < kv.first.size(); ++i) { if (kv.first[i] != str[i]) { match = false; break; } } bool no_space_between_short_arg_and_value = str.size() > kv.first.size(); if (match && no_space_between_short_arg_and_value) { //Only handles cases where there is no space between short arg and value short_arg_info.is_no_space_short_arg = true; short_arg_info.arg = kv.second; short_arg_info.value = std::string(str.begin() + kv.first.size(), str.end()); return short_arg_info; } } } assert(!short_arg_info.is_no_space_short_arg); return short_arg_info; } /* * ArgumentGroup */ ArgumentGroup::ArgumentGroup(std::string name_str) : name_(name_str) {} ArgumentGroup& ArgumentGroup::epilog(std::string str) { epilog_ = str; return *this; } std::string ArgumentGroup::name() const { return name_; } std::string ArgumentGroup::epilog() const { return epilog_; } const std::vector>& ArgumentGroup::arguments() const { return arguments_; } /* * Argument */ Argument::Argument(std::string long_opt, std::string short_opt) : long_opt_(long_opt) , short_opt_(short_opt) { if (long_opt_.size() < 1) { throw ArgParseError("Argument must be at least one character long"); } auto dashes_name = split_leading_dashes(long_opt_); if (dashes_name[0].size() == 1 && !short_opt_.empty()) { throw ArgParseError("Long option must be specified before short option"); } else if (dashes_name[0].size() > 2) { throw ArgParseError("More than two dashes in argument name"); } //Set defaults metavar_ = toupper(dashes_name[1]); } Argument& Argument::help(std::string help_str) { help_ = help_str; return *this; } Argument& Argument::nargs(char nargs_type) { //TODO: nargs > 1 support: '?', '*', '+' auto valid_nargs = {'0', '1', '+', '*'}; auto iter = std::find(valid_nargs.begin(), valid_nargs.end(), nargs_type); if (iter == valid_nargs.end()) { throw ArgParseError("Invalid argument to nargs (must be one of: " + join(valid_nargs, ", ") + ")"); } //Ensure nargs is consistent with the action if (action() == Action::STORE_FALSE && nargs_type != '0') { throw ArgParseError("STORE_FALSE action requires nargs to be '0'"); } else if (action() == Action::STORE_TRUE && nargs_type != '0') { throw ArgParseError("STORE_TRUE action requires nargs to be '0'"); } else if (action() == Action::HELP && nargs_type != '0') { throw ArgParseError("HELP action requires nargs to be '0'"); } else if (action() == Action::STORE && (nargs_type != '1' && nargs_type != '+' && nargs_type != '*')) { throw ArgParseError("STORE action requires nargs to be '1', '+' or '*'"); } nargs_ = nargs_type; valid_action(); return *this; } Argument& Argument::metavar(std::string metavar_str) { metavar_ = metavar_str; return *this; } Argument& Argument::choices(std::vector choice_values) { choices_ = choice_values; return *this; } Argument& Argument::action(Action action_type) { action_ = action_type; if ( action_ == Action::STORE_FALSE || action_ == Action::STORE_TRUE || action_ == Action::HELP || action_ == Action::VERSION) { this->nargs('0'); } else if (action_ == Action::STORE) { this->nargs('1'); } else { throw ArgParseError("Unrecognized argparse action"); } return *this; } Argument& Argument::required(bool is_required) { required_ = is_required; return *this; } Argument& Argument::default_value(const std::string& value) { if (nargs() != '0' && nargs() != '1' && nargs() != '?') { std::stringstream msg; msg << "Scalar default value not allowed for nargs='" << nargs() << "'"; throw ArgParseError(msg.str()); } default_value_.clear(); default_value_.push_back(value); default_set_ = true; return *this; } Argument& Argument::default_value(const std::vector& values) { if (nargs() != '+' && nargs() != '*') { std::stringstream msg; msg << "Multiple default value not allowed for nargs='" << nargs() << "'"; throw ArgParseError(msg.str()); } default_value_ = values; default_set_ = true; return *this; } Argument& Argument::default_value(const std::initializer_list& values) { //Convert to vector and process as usual return default_value(std::vector(values.begin(), values.end())); } Argument& Argument::group_name(std::string grp) { group_name_ = grp; return *this; } Argument& Argument::show_in(ShowIn show) { show_in_ = show; return *this; } std::string Argument::name() const { std::string name_str = long_option(); if (!short_option().empty()) { name_str += "/" + short_option(); } return name_str; } std::string Argument::long_option() const { return long_opt_; } std::string Argument::short_option() const { return short_opt_; } std::string Argument::help() const { return help_; } char Argument::nargs() const { return nargs_; } std::string Argument::metavar() const { return metavar_; } std::vector Argument::choices() const { return choices_; } Action Argument::action() const { return action_; } std::string Argument::default_value() const { if (default_value_.size() > 1) { std::stringstream msg; msg << "{" << join(default_value_, ", ") << "}"; return msg.str(); } else if (default_value_.size() == 1) { return default_value_[0]; } else { return ""; } } std::string Argument::group_name() const { return group_name_; } ShowIn Argument::show_in() const { return show_in_; } bool Argument::default_set() const { return default_set_; } bool Argument::required() const { if(positional()) { //Positional arguments are always required return true; } return required_; } bool Argument::positional() const { assert(long_option().size() > 1); return long_option()[0] != '-'; } } //namespace