#pragma once #include <cstring> #include <iomanip> #include <iostream> #include <map> #include "common.hpp" LEMONBUDDY_NS namespace command_line { DEFINE_ERROR(argument_error); DEFINE_ERROR(value_error); class option; using choices = vector<string>; using options = vector<option>; using values = map<string, string>; // class definition : option {{{ class option { public: string flag; string flag_long; string desc; string token; choices values; /** * Construct option */ explicit option( string&& flag, string&& flag_long, string&& desc, string&& token = "", choices&& c = {}) : flag(forward<string>(flag)) , flag_long(forward<string>(flag_long)) , desc(forward<string>(desc)) , token(forward<string>(token)) , values(forward<choices>(c)) {} }; // }}} // class definition : parser {{{ class parser { public: /** * Construct parser */ explicit parser(string&& synopsis, const options& opts) : m_synopsis(forward<string>(synopsis)), m_opts(opts) {} /** * Process input values * * This is done outside the constructor due to boost::di noexcept */ void process_input(const vector<string>& values) { for (size_t i = 0; i < values.size(); i++) { parse(values[i], values.size() > i + 1 ? values[i + 1] : ""); } } /** * Test if the passed option was provided */ bool has(string&& option) const { return m_optvalues.find(forward<string>(option)) != m_optvalues.end(); } /** * Compares the option value with given string */ bool compare(string&& opt, string val) const { return get(forward<string>(opt)) == val; } /** * Gets the value defined for given option */ string get(string&& opt) const { if (has(forward<string>(opt))) return m_optvalues.find(forward<string>(opt))->second; return ""; } /** * Prints application usage message */ void usage() const { std::cout << m_synopsis << "\n" << std::endl; // get the length of the longest string in the flag column // which is used to align the description fields size_t maxlen{0}; for (auto it = m_opts.begin(); it != m_opts.end(); ++it) { size_t len{it->flag_long.length() + it->flag.length() + 4}; maxlen = len > maxlen ? len : maxlen; } for (auto& opt : m_opts) { int pad = maxlen - opt.flag_long.length() - opt.token.length(); std::cout << " " << opt.flag << ", " << opt.flag_long; if (!opt.token.empty()) { std::cout << "=" << opt.token; pad--; } // output the list with accepted values if (!opt.values.empty()) { std::cout << std::setw(pad + opt.desc.length()) << std::setfill(' ') << opt.desc << std::endl; pad = pad + opt.flag_long.length() + opt.token.length() + 7; std::cout << string(pad, ' ') << opt.token << " is one of: "; for (auto& v : opt.values) { std::cout << v << (v != opt.values.back() ? ", " : ""); } } else { std::cout << std::setw(pad + opt.desc.length()) << std::setfill(' ') << opt.desc; } std::cout << std::endl; } } /** * Configure injection module */ template <class T = parser> static di::injector<T> configure(string scriptname, const options& opts) { // clang-format off return di::make_injector( di::bind<>().to("Usage: " + scriptname + " bar_name [OPTION...]"), di::bind<>().to(opts)); // clang-format on } protected: /** * Compare option with its short version */ auto is_short(string option, string opt_short) const { return option.compare(0, opt_short.length(), opt_short) == 0; } /** * Compare option with its long version */ auto is_long(string option, string opt_long) const { return option.compare(0, opt_long.length(), opt_long) == 0; } /** * Compare option with both versions */ auto is(string option, string opt_short, string opt_long) const { return is_short(option, opt_short) || is_long(option, opt_long); } /** * Gets value defined for */ auto parse_value(string input, string input_next, choices values) const { string opt = input; size_t pos; string value; if (input_next.empty() && opt.compare(0, 2, "--") != 0) throw value_error("Missing value for " + opt); else if ((pos = opt.find("=")) == string::npos && opt.compare(0, 2, "--") == 0) throw value_error("Missing value for " + opt); else if (pos == string::npos && !input_next.empty()) value = input_next; else { value = opt.substr(pos + 1); opt = opt.substr(0, pos); } if (!values.empty() && std::find(values.begin(), values.end(), value) == values.end()) throw value_error("Invalid value '" + value + "' for argument " + string{opt}); return value; } /** * Parses and validates passed arguments and flags */ void parse(string input, string input_next = "") { if (m_skipnext) { m_skipnext = false; if (!input_next.empty()) return; } for (auto&& opt : m_opts) { if (is(input, opt.flag, opt.flag_long)) { if (opt.token.empty()) { m_optvalues.insert(std::make_pair(opt.flag_long.substr(2), "")); } else { auto value = parse_value(input, input_next, opt.values); m_skipnext = (value == input_next); m_optvalues.insert(std::make_pair(opt.flag_long.substr(2), value)); } return; } } if (input.compare(0, 1, "-") == 0) { throw argument_error("Unrecognized option " + input); } } private: string m_synopsis; options m_opts; values m_optvalues; bool m_skipnext = false; }; // }}} } LEMONBUDDY_NS_END