You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
F4KXL_DStarGateway/BaseCommon/optionparser.h

700 lines
21 KiB

//-----------------------------------------------------------------------------
// optionparser.h -- A Header-Only commandline argument parser
// Author: Luke de Oliveira <lukedeo@ldo.io>
// License: MIT
//-----------------------------------------------------------------------------
#ifndef OPTIONPARSER_H_
#define OPTIONPARSER_H_
#include <iostream>
#include <map>
#include <numeric>
#include <sstream>
#include <utility>
#include <vector>
#include <algorithm>
namespace optionparser {
// The utils::* namespace contains general utilities not necessarily useful
// outside the main scope of the library
namespace utils {
std::vector<std::string> split_str(std::string s,
const std::string &delimiter = " ") {
size_t pos = 0;
size_t delimiter_length = delimiter.length();
std::vector<std::string> vals;
while ((pos = s.find(delimiter)) != std::string::npos) {
vals.push_back(s.substr(0, pos));
s.erase(0, pos + delimiter_length);
}
vals.push_back(s);
return vals;
}
std::string stitch_str(const std::vector<std::string> &text,
unsigned max_per_line = 80,
const std::string &leading_str = "") {
std::vector<std::string> result;
std::string line_value;
for (const auto &token : text) {
if (line_value.empty()) {
line_value = (leading_str + token);
continue;
}
auto hypothetical_line = line_value;
hypothetical_line.append(" " + token);
if (hypothetical_line.size() > max_per_line) {
// In this case, we were better off before
result.emplace_back(line_value);
line_value = (leading_str + token);
} else {
line_value = hypothetical_line;
}
}
// Collect the last line since we don't track indices in the loop proper.
result.emplace_back(line_value);
return std::accumulate(
result.begin() + 1, result.end(), result.at(0),
[](std::string &s, const std::string &piece) -> std::string {
return s + "\n" + piece;
});
}
} // end namespace utils
// Define a thin error for any sort of parser error that arises
class ParserError : public std::runtime_error {
using std::runtime_error::runtime_error;
};
// Enums for Option config
enum StorageMode { STORE_TRUE = 0, STORE_VALUE, STORE_MULT_VALUES };
enum OptionType { LONG_OPT = 0, SHORT_OPT, POSITIONAL_OPT, EMPTY_OPT };
// Option class definition
class Option {
public:
Option() = default;
std::string help_doc();
std::string &short_flag() { return m_short_flag; }
std::string &long_flag() { return m_long_flag; }
std::string &pos_flag() { return m_pos_flag; }
bool found() { return m_found; }
Option &found(bool found) {
m_found = found;
return *this;
}
StorageMode mode() { return m_mode; }
Option &mode(const StorageMode &mode) {
m_mode = mode;
return *this;
}
bool required() { return m_required; }
Option &required(bool req) {
m_required = req;
return *this;
}
std::string metavar() {
std::string formatted_metavar;
if (!m_metavar.empty()) {
if (m_mode == STORE_TRUE) {
return "";
}
formatted_metavar = m_metavar;
} else {
for (const auto &cand :
{m_dest, m_pos_flag, m_long_flag, std::string("ARG")}) {
if (!cand.empty()) {
formatted_metavar = cand.substr(cand.find_first_not_of('-'));
std::transform(formatted_metavar.begin(), formatted_metavar.end(),
formatted_metavar.begin(), ::toupper);
break;
}
}
}
if (m_mode == STORE_MULT_VALUES) {
formatted_metavar = (formatted_metavar + "1 [" + formatted_metavar +
"2, " + formatted_metavar + "3, ...]");
}
return formatted_metavar;
}
Option &metavar(const std::string &mvar) {
m_metavar = mvar;
return *this;
}
std::string help() { return m_help; }
Option &help(const std::string &help) {
m_help = help;
return *this;
}
std::string dest() { return m_dest; }
Option &dest(const std::string &dest) {
m_dest = dest;
return *this;
}
std::string default_value() { return m_default_value; }
Option &default_value(const std::string &default_value) {
m_default_value = default_value;
return *this;
}
Option &default_value(const char *default_value) {
m_default_value = std::string(default_value);
return *this;
}
template <typename T> Option &default_value(const T &default_value) {
m_default_value = std::to_string(default_value);
return *this;
}
static OptionType get_type(std::string opt);
static std::string get_destination(const std::string &first_option,
const std::string &second_option);
static void validate_option_types(const OptionType &first_option_type,
const OptionType &second_option_type);
private:
bool m_found = false;
bool m_required = false;
StorageMode m_mode = STORE_TRUE;
std::string m_help = "";
std::string m_dest = "";
std::string m_default_value = "";
std::string m_metavar = "";
std::string m_short_flag = "";
std::string m_long_flag = "";
std::string m_pos_flag = "";
};
// Non-inline definitions for Option methods
std::string Option::help_doc() {
std::string h = " ";
if (!m_long_flag.empty()) {
h += m_long_flag;
if (!m_short_flag.empty()) {
h += ", ";
}
}
if (!m_short_flag.empty()) {
h += m_short_flag;
}
if (!m_pos_flag.empty()) {
h += m_pos_flag;
}
auto arg_buf = std::max(h.length() + 1, static_cast<unsigned long>(25));
auto help_str = utils::stitch_str(utils::split_str(m_help), arg_buf + 50,
std::string(arg_buf, ' '));
char char_buf[h.length() + help_str.length() + 100];
sprintf(char_buf, ("%-" + std::to_string(arg_buf) + "s%s\n").c_str(),
h.c_str(), help_str.substr(arg_buf).c_str());
return std::string(char_buf);
}
OptionType Option::get_type(std::string opt) {
if (opt.empty()) {
return OptionType::EMPTY_OPT;
}
if (opt.size() == 2) {
if (opt[0] == '-') {
return OptionType::SHORT_OPT;
}
}
if (opt.size() > 2) {
if (opt[0] == '-' && opt[1] == '-') {
return OptionType::LONG_OPT;
}
}
return OptionType::POSITIONAL_OPT;
}
void Option::validate_option_types(const OptionType &first_option_type,
const OptionType &second_option_type) {
auto err = [](const std::string &msg) {
throw std::runtime_error("Parser inconsistency: " + msg);
};
if (first_option_type == OptionType::EMPTY_OPT) {
err("Cannot have first option be empty.");
}
if (first_option_type == OptionType::POSITIONAL_OPT &&
second_option_type != OptionType::EMPTY_OPT) {
err("Positional arguments can only have one option, found non-empty second "
"option.");
}
if (second_option_type == OptionType::POSITIONAL_OPT) {
err("Cannot have second option be a positional option.");
}
}
std::string Option::get_destination(const std::string &first_option,
const std::string &second_option) {
std::string dest;
auto first_opt_type = Option::get_type(first_option);
auto second_opt_type = Option::get_type(second_option);
validate_option_types(first_opt_type, second_opt_type);
if (first_opt_type == OptionType::LONG_OPT) {
dest = first_option.substr(2);
} else if (second_opt_type == OptionType::LONG_OPT) {
dest = second_option.substr(2);
} else {
if (first_opt_type == OptionType::SHORT_OPT) {
dest = first_option.substr(1) + "_option";
} else if (second_opt_type == OptionType::SHORT_OPT) {
dest = second_option.substr(1) + "_option";
} else {
if (first_opt_type == OptionType::POSITIONAL_OPT &&
second_opt_type == OptionType::EMPTY_OPT) {
dest = first_option;
} else {
std::string msg = "Parser inconsistency error.";
throw std::runtime_error(msg);
}
}
}
return dest;
}
// OptionParser class definition
class OptionParser {
public:
explicit OptionParser(std::string description = "", bool create_help = true)
: m_options(0), m_description(std::move(description)),
m_pos_args_count(1), m_exit_on_failure(true) {
if (create_help) {
add_option("--help", "-h").help("Display this help message and exit.");
}
}
~OptionParser() = default;
void eat_arguments(unsigned int argc, char const *argv[]);
Option &add_option(const std::string &first_option,
const std::string &second_option = "");
// We template-specialize these later
template <class T = bool> T get_value(const std::string &key);
void help();
OptionParser &exit_on_failure(bool exit = true);
OptionParser &throw_on_failure(bool throw_ = true);
private:
Option &add_option_internal(const std::string &first_option,
const std::string &second_option);
void try_to_exit_with_message(const std::string &e);
ParserError fail_for_missing_key(const std::string &key);
ParserError fail_unrecognized_argument(const std::string &arg);
ParserError
fail_for_missing_arguments(const std::vector<std::string> &missing_flags);
bool get_value_arg(std::vector<std::string> &arguments, unsigned int &arg,
Option &opt, std::string &flag);
bool try_to_get_opt(std::vector<std::string> &arguments, unsigned int &arg,
Option &option, std::string &flag);
void check_for_missing_args();
std::vector<Option> m_options;
std::string m_prog_name, m_description;
int m_pos_args_count;
bool m_exit_on_failure;
std::map<std::string, std::vector<std::string>> m_values;
std::vector<std::string> m_positional_options_names;
std::map<std::string, unsigned int> m_option_idx;
};
// Define methods non-inline
Option &OptionParser::add_option(const std::string &first_option,
const std::string &second_option) {
return add_option_internal(first_option, second_option);
}
Option &OptionParser::add_option_internal(const std::string &first_option,
const std::string &second_option) {
m_options.resize(m_options.size() + 1);
Option &opt = m_options.back();
OptionType first_option_type = Option::get_type(first_option);
OptionType second_option_type = Option::get_type(second_option);
try {
opt.dest(Option::get_destination(first_option, second_option));
} catch (const std::runtime_error &err) {
try_to_exit_with_message(err.what());
throw err;
}
if (first_option_type == OptionType::LONG_OPT) {
opt.long_flag() = first_option;
} else if (second_option_type == OptionType::LONG_OPT) {
opt.long_flag() = second_option;
}
if (first_option_type == OptionType::SHORT_OPT) {
opt.short_flag() = first_option;
} else if (second_option_type == OptionType::SHORT_OPT) {
opt.short_flag() = second_option;
}
if (first_option_type == OptionType::POSITIONAL_OPT) {
opt.pos_flag() = first_option;
m_pos_args_count += 1;
m_positional_options_names.push_back(first_option);
}
return opt;
}
bool OptionParser::get_value_arg(std::vector<std::string> &arguments,
unsigned int &arg, Option &opt,
std::string &flag) {
std::string val;
m_values[opt.dest()].clear();
if (arguments[arg].size() > flag.size()) {
auto search_pt = arguments[arg].find_first_of('=');
if (search_pt == std::string::npos) {
search_pt = arguments[arg].find_first_of(' ');
if (search_pt == std::string::npos) {
try_to_exit_with_message("Error, long options (" + flag +
") require a '=' or space before a value.");
return false;
}
auto vals = utils::split_str(arguments[arg].substr(search_pt + 1));
for (const auto &v : vals) {
m_values[opt.dest()].push_back(v);
}
}
} else {
if (arg + 1 >= arguments.size()) {
if (opt.default_value().empty()) {
try_to_exit_with_message("error, flag '" + flag +
"' requires an argument.");
return false;
}
if (m_values[opt.dest()].empty()) {
val = opt.default_value();
}
} else {
if (arguments[arg + 1][0] == '-') {
if (opt.default_value().empty()) {
try_to_exit_with_message("error, flag '" + flag +
"' requires an argument.");
return false;
}
if (m_values[opt.dest()].empty()) {
val = opt.default_value();
}
}
}
}
if (!val.empty()) {
m_values[opt.dest()].push_back(val);
return true;
}
int arg_distance = 0;
while (arguments[arg + 1][0] != '-') {
arg++;
if (arg_distance && (opt.mode() != StorageMode::STORE_MULT_VALUES)) {
break;
}
arg_distance++;
m_values[opt.dest()].push_back(arguments[arg]);
if (arg + 1 >= arguments.size()) {
break;
}
}
return true;
}
bool OptionParser::try_to_get_opt(std::vector<std::string> &arguments,
unsigned int &arg, Option &option,
std::string &flag) {
if (flag.empty()) {
return false;
}
if (arguments[arg] != flag) {
return false;
}
if (!option.pos_flag().empty()) {
m_values[option.dest()].push_back(option.pos_flag());
option.found(true);
return true;
}
if (option.mode() == STORE_TRUE) {
option.found(true);
return true;
}
if (((option.mode() == STORE_VALUE) ||
(option.mode() == STORE_MULT_VALUES)) &&
!option.found()) {
if (get_value_arg(arguments, arg, option, flag)) {
option.found(true);
return true;
}
}
return false;
}
void OptionParser::check_for_missing_args() {
std::vector<std::string> missing;
for (auto &opt : m_options) {
if ((opt.required()) && (!opt.found())) {
missing.push_back(opt.dest());
} else if ((!opt.default_value().empty()) && (!opt.found())) {
m_values[opt.dest()].push_back(opt.default_value());
opt.found(true);
}
}
if (!missing.empty()) {
throw fail_for_missing_arguments(missing);
}
}
void OptionParser::eat_arguments(unsigned int argc, char const *argv[]) {
unsigned int idx_ctr = 0;
for (auto &opt : m_options) {
m_option_idx[opt.dest()] = idx_ctr;
idx_ctr++;
}
const std::string args_end = "- ";
m_prog_name = argv[0];
std::vector<std::string> arguments(argv + 1, argv + argc);
// dummy way to solve problem with last arg of
arguments.emplace_back(args_end);
// for each argument cluster
int pos_args = 1;
for (unsigned int arg = 0; arg < arguments.size(); ++arg) {
bool match_found = false;
// for each option sets
for (auto &option : m_options) {
match_found = try_to_get_opt(arguments, arg, option, option.long_flag());
if (match_found) {
break;
}
match_found = try_to_get_opt(arguments, arg, option, option.short_flag());
if (match_found) {
break;
}
}
if (!match_found) {
if (arguments[arg] != args_end) {
if (m_pos_args_count > pos_args) {
m_options[m_option_idx.at(m_positional_options_names[pos_args - 1])]
.found(true);
m_values[m_positional_options_names[pos_args - 1]].push_back(
arguments[arg]);
pos_args++;
} else
throw fail_unrecognized_argument(arguments[arg]);
}
}
}
if (get_value("help")) {
help();
}
check_for_missing_args();
}
void OptionParser::try_to_exit_with_message(const std::string &e) {
if (m_exit_on_failure) {
std::cerr << "In excecutable \'";
std::cerr << m_prog_name << "\':\n" << e << std::endl;
exit(1);
}
}
ParserError OptionParser::fail_for_missing_key(const std::string &key) {
auto msg = "Tried to access value for field '" + key +
"' which is not a valid field.";
try_to_exit_with_message(msg);
return ParserError(msg);
}
ParserError OptionParser::fail_unrecognized_argument(const std::string &arg) {
auto msg = "Unrecognized flag/option '" + arg + "'";
try_to_exit_with_message(msg);
return ParserError(msg);
}
ParserError OptionParser::fail_for_missing_arguments(
const std::vector<std::string> &missing_flags) {
auto msg =
"Missing required flags: " +
std::accumulate(
missing_flags.begin() + 1, missing_flags.end(), missing_flags.at(0),
[](std::string &s, const std::string &piece) -> std::string {
return s + ", " + piece;
}) +
".";
try_to_exit_with_message(msg);
return ParserError(msg);
}
void OptionParser::help() {
auto split = m_prog_name.find_last_of('/');
std::stringstream leading;
leading << "usage: " << m_prog_name.substr(split + 1) << " ";
std::string usage_str = leading.str();
std::cout << usage_str;
std::vector<std::string> option_usage;
for (auto &option : m_options) {
std::stringstream optss;
optss << (option.required() ? "" : "[");
if (!option.short_flag().empty()) {
optss << option.short_flag();
} else if (!option.long_flag().empty()) {
optss << option.long_flag();
}
if (option.mode() != StorageMode::STORE_TRUE) {
optss << (option.pos_flag().empty() ? " " : "") << option.metavar();
}
optss << (option.required() ? " " : "] ");
option_usage.emplace_back(optss.str());
}
std::cout << utils::stitch_str(option_usage, 80,
std::string(usage_str.size(), ' '))
.substr(usage_str.size())
<< std::endl;
if (!m_description.empty()) {
std::cout << "\n" << m_description << "\n" << std::endl;
}
std::vector<Option> pos_opts;
std::vector<Option> reg_opts;
std::copy_if(m_options.begin(), m_options.end(), std::back_inserter(pos_opts),
[](Option &o) { return !o.pos_flag().empty(); });
std::copy_if(m_options.begin(), m_options.end(), std::back_inserter(reg_opts),
[](Option &o) { return o.pos_flag().empty(); });
if (!pos_opts.empty()) {
std::cout << "\nPositional Arguments:" << std::endl;
std::for_each(pos_opts.begin(), pos_opts.end(),
[](Option &o) { std::cout << o.help_doc(); });
}
std::cout << "\nOptions:" << std::endl;
std::for_each(reg_opts.begin(), reg_opts.end(),
[](Option &o) { std::cout << o.help_doc(); });
exit(0);
}
OptionParser &OptionParser::exit_on_failure(bool exit) {
m_exit_on_failure = exit;
return *this;
}
OptionParser &OptionParser::throw_on_failure(bool throw_) {
m_exit_on_failure = !throw_;
return *this;
}
template <class T> T OptionParser::get_value(const std::string &key) {
try {
return m_options[m_option_idx.at(key)].found();
} catch (std::out_of_range &err) {
throw fail_for_missing_key(key);
}
}
// Provide all template specializations for get_value<T>(keyName)
#define GET_VALUE_SPECIALIZE(type, code) \
template <> type OptionParser::get_value<type>(const std::string &key) { \
try { \
code \
} catch (std::out_of_range & err) { \
throw fail_for_missing_key(key); \
} \
}
GET_VALUE_SPECIALIZE(std::string, { return m_values.at(key).at(0); })
GET_VALUE_SPECIALIZE(const char *, { return m_values.at(key).at(0).c_str(); })
GET_VALUE_SPECIALIZE(double, { return std::stod(m_values.at(key).at(0)); })
GET_VALUE_SPECIALIZE(float, { return std::stof(m_values.at(key).at(0)); })
GET_VALUE_SPECIALIZE(int, { return std::stoi(m_values.at(key).at(0)); })
GET_VALUE_SPECIALIZE(unsigned int,
{ return std::stoul(m_values.at(key).at(0)); })
GET_VALUE_SPECIALIZE(std::vector<std::string>, { return m_values.at(key); })
#define GET_VALUE_SPECIALIZE_VECTOR(type, converter) \
GET_VALUE_SPECIALIZE(std::vector<type>, { \
std::vector<type> v; \
for (auto &entry : m_values.at(key)) { \
v.push_back(converter(entry)); \
} \
return /*std::move(v);*/v; \
})
GET_VALUE_SPECIALIZE_VECTOR(const char *,
[](const std::string &s) { return s.c_str(); })
GET_VALUE_SPECIALIZE_VECTOR(int, std::stoi)
GET_VALUE_SPECIALIZE_VECTOR(unsigned int, std::stoul)
GET_VALUE_SPECIALIZE_VECTOR(float, std::stof)
GET_VALUE_SPECIALIZE_VECTOR(double, std::stod)
} // end namespace optionparser
#endif

Powered by TurnKey Linux.