Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:munix9
AppImageUpdate
argagg.hpp
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File argagg.hpp of Package AppImageUpdate
/* * @file * @brief * Defines a very simple command line argument parser. * * @copyright * Copyright (c) 2018 Viet The Nguyen * * @copyright * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * @copyright * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * @copyright * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ #pragma once #ifndef ARGAGG_ARGAGG_ARGAGG_HPP #define ARGAGG_ARGAGG_ARGAGG_HPP #include <algorithm> #include <array> #include <cctype> #include <cstdlib> #include <cstring> #include <iterator> #include <ostream> #include <sstream> #include <stdexcept> #include <string> #include <unordered_map> #include <utility> #include <vector> /** * @brief * There are only two hard things in Computer Science: cache invalidation and * naming things (Phil Karlton). * * The names of types have to be succinct and clear. This has turned out to be * a more difficult thing than I expected. Here you'll find a quick overview of * the type names you'll find in this namespace (and thus "library"). * * When a program is invoked it is passed a number of "command line arguments". * Each of these "arguments" is a string (C-string to be more precise). An * "option" is a command line argument that has special meaning. This library * recognizes a command line argument as a potential option if it starts with a * dash ('-') or double-dash ('--'). * * A "parser" is a set of "definitions" (not a literal std::set but rather a * std::vector). A parser is represented by the argagg::parser struct. * * A "definition" is a structure with four components that define what * "options" are recognized. The four components are the name of the option, * the strings that represent the option, the option's help text, and how many * arguments the option should expect. "Flags" are the individual strings that * represent the option ("-v" and "--verbose" are flags for the "verbose" * option). A definition is represented by the argagg::definition struct. * * Note at this point that the word "option" can be used interchangeably to * mean the notion of an option and the actual instance of an option given a * set of command line arguments. To be unambiguous we use a "definition" to * represent the notion of an option and an "option result" to represent an * actual option parsed from a set of command line arguments. An "option * result" is represented by the argagg::option_result struct. * * There's one more wrinkle to this: an option can show up multiple times in a * given set of command line arguments. For example, "-n 1 -n 2 -n 3". This * will parse into three distinct argagg::option_result instances, but all of * them correspond to the same argagg::definition. We aggregate these into the * argagg::option_results struct which represents "all parser results for a * given option definition". This argagg::option_results is basically a * std::vector of argagg::option_result. * * Options aren't the only thing parsed though. Positional arguments are also * parsed. Thus a parser produces a result that contains both option results * and positional arguments. The parser results are represented by the * argagg::parser_results struct. All option results are stored in a mapping * from option name to the argagg::option_results. All positional arguments are * simply stored in a vector of C-strings. */ namespace argagg { /** * @brief * This exception is thrown when a long option is parsed and is given an * argument using the "=" syntax but the option doesn't expect an argument. */ struct unexpected_argument_error : public std::runtime_error { using std::runtime_error::runtime_error; }; /** * @brief * This exception is thrown when an option is parsed unexpectedly such as when * an argument was expected for a previous option or if an option was found * that has not been defined. */ struct unexpected_option_error : public std::runtime_error { using std::runtime_error::runtime_error; }; /** * @brief * This exception is thrown when an option requires an argument but is not * provided one. This can happen if another flag was found after the option or * if we simply reach the end of the command line arguments. */ struct option_lacks_argument_error : public std::runtime_error { using std::runtime_error::runtime_error; }; /** * @brief * This exception is thrown when an option's flag is invalid. This can be the * case if the flag is not prefixed by one or two hyphens or contains non * alpha-numeric characters after the hyphens. See is_valid_flag_definition() * for more details. */ struct invalid_flag : public std::runtime_error { using std::runtime_error::runtime_error; }; /** * @brief * This exception is thrown when an unknown option is requested by name from an * argagg::parser_results through the indexing operator ([]). */ struct unknown_option : public std::runtime_error { using std::runtime_error::runtime_error; }; /** * @brief * The set of template instantiations that convert C-strings to other types for * the option_result::as(), option_results::as(), parser_results::as(), and * parser_results::all_as() methods are placed in this namespace. */ namespace convert { /** * @brief * Explicit instantiations of this function are used to convert arguments to * types. */ template <typename T> T arg(const char* arg); /** * @brief * For simple types the main extension point for adding argument conversions * is argagg::convert::arg<T>(). However, for complex types such as templated * types partial specialization of a helper struct is required. This struct * provides that extension point. The default, generic implementation of * argagg::convert::arg<T>() calls converter<T>::convert(). * * @see * @ref argagg::csv */ template <typename T> struct converter { static T convert(const char* arg); }; /** * @brief * A utility function for parsing an argument as a delimited list. To use, * initialize a const char* pointer to the start of argument string. Then * call parse_next_component(), providing that pointer, a mutable reference * to where the parsed argument will go, and optionally the delimiting * character. The argument string will be read up to the next delimiting * character and then converted using * <tt>argagg::convert::arg<decltype(out_arg)>()</tt>. The pointer is then * incremented accordingly. If the delimiting character is no longer found * then false is returned meaning that parsing the list can be considered * finished. * * @code #include <argagg/argagg.hpp> struct position3 { double x; double y; double z; }; namespace argagg { namespace convert { template <> position3 arg(const char* s) { position3 result {0.0, 0.0, 0.0}; if (!parse_next_component(s, result.x)) { // could potentially throw an error if you require that at least two // components exist in the list return result; } if (!parse_next_component(s, result.y)) { return result; } if (!parse_next_component(s, result.z)) { return result; } return result; } } // namespace convert } // namespace argagg int main(int argc, char** argv) { argagg::parser argparser {{ { "origin", {"-o", "--origin"}, "origin as position3 specified as a comma separated list of " "components (e.g. '1,2,3')", 1}, }}; argagg::parser_results args = argparser.parse(argc, argv); auto my_position = args["origin"].as<position3>(); // ... return 0; } @endcode */ template <typename T> bool parse_next_component( const char*& s, T& out_arg, const char delim = ','); } /** * @brief * Represents a single option parse result. * * You can check if this has an argument by using the implicit boolean * conversion. */ struct option_result { /** * @brief * Argument parsed for this single option. If no argument was parsed this * will be set to nullptr. */ const char* arg; /** * @brief * Converts the argument parsed for this single option instance into the * given type using the type matched conversion function * argagg::convert::arg(). If there was not an argument parsed for this * single option instance then a argagg::option_lacks_argument_error * exception is thrown. The specific conversion function may throw other * exceptions. */ template <typename T> T as() const; /** * @brief * Converts the argument parsed for this single option instance into the * given type using the type matched conversion function * argagg::convert::arg(). If there was not an argument parsed for this * single option instance then the provided default value is returned * instead. If the conversion function throws an exception then it is ignored * and the default value is returned. */ template <typename T> T as(const T& t) const; /** * @brief * Since we have the argagg::option_result::as() API we might as well alias * it as an implicit conversion operator. This performs implicit conversion * using the argagg::option_result::as() method. * * @note * An implicit boolean conversion specialization exists which returns false * if there is no argument for this single option instance and true * otherwise. This specialization DOES NOT convert the argument to a bool. If * you need to convert the argument to a bool then use the as() API. */ template <typename T> operator T () const; /** * @brief * Explicitly define a unary not operator that wraps the implicit boolean * conversion specialization in case the compiler can't do it automatically. */ bool operator ! () const; }; /** * @brief * Represents multiple option parse results for a single option. If treated as * a single parse result it defaults to the last parse result. Note that an * instance of this struct is always created even if no option results are * parsed for a given definition. In that case it will simply be empty. * * To check if the associated option showed up at all simply use the implicit * boolean conversion or check if count() is greater than zero. */ struct option_results { /** * @brief * All option parse results for this option. */ std::vector<option_result> all; /** * @brief * Gets the number of times the option shows up. */ std::size_t count() const; /** * @brief * Gets a single option parse result by index. */ option_result& operator [] (std::size_t index); /** * @brief * Gets a single option result by index. */ const option_result& operator [] (std::size_t index) const; /** * @brief * Converts the argument parsed for the LAST option parse result for the * parent definition to the provided type. For example, if this was for "-f 1 * -f 2 -f 3" then calling this method for an integer type will return 3. If * there are no option parse results then a std::out_of_range exception is * thrown. Any exceptions thrown by option_result::as() are not * handled. */ template <typename T> T as() const; /** * @brief * Converts the argument parsed for the LAST option parse result for the * parent definition to the provided type. For example, if this was for "-f 1 * -f 2 -f 3" then calling this method for an integer type will return 3. If * there are no option parse results then the provided default value is * returned instead. */ template <typename T> T as(const T& t) const; /** * @brief * Since we have the option_results::as() API we might as well alias * it as an implicit conversion operator. This performs implicit conversion * using the option_results::as() method. * * @note * An implicit boolean conversion specialization exists which returns false * if there is no argument for this single option instance and true * otherwise. This specialization DOES NOT convert the argument to a bool. If * you need to convert the argument to a bool then use the as() API. */ template <typename T> operator T () const; /** * @brief * Explicitly define a unary not operator that wraps the implicit boolean * conversion specialization in case the compiler can't do it automatically. */ bool operator ! () const; }; /** * @brief * Represents all results of the parser including options and positional * arguments. */ struct parser_results { /** * @brief * Returns the name of the program from the original arguments list. This is * always the first argument. */ const char* program; /** * @brief * Maps from definition name to the structure which contains the parser * results for that definition. */ std::unordered_map<std::string, option_results> options; /** * @brief * Vector of positional arguments. */ std::vector<const char*> pos; /** * @brief * Used to check if an option was specified at all. */ bool has_option(const std::string& name) const; /** * @brief * Get the parser results for the given definition. If the definition never * showed up then the exception from the unordered_map access will bubble * through so check if the flag exists in the first place with has_option(). */ option_results& operator [] (const std::string& name); /** * @brief * Get the parser results for the given definition. If the definition never * showed up then the exception from the unordered_map access will bubble * through so check if the flag exists in the first place with has_option(). */ const option_results& operator [] (const std::string& name) const; /** * @brief * Gets the number of positional arguments. */ std::size_t count() const; /** * @brief * Gets a positional argument by index. */ const char* operator [] (std::size_t index) const; /** * @brief * Gets a positional argument converted to the given type. */ template <typename T> T as(std::size_t i = 0) const; /** * @brief * Gets all positional arguments converted to the given type. */ template <typename T> std::vector<T> all_as() const; }; /** * @brief * An option definition which essentially represents what an option is. */ struct definition { /** * @brief * Name of the option. Option parser results are keyed by this name. */ const std::string name; /** * @brief * List of strings to match that correspond to this option. Should be fully * specified with hyphens (e.g. "-v" or "--verbose"). */ std::vector<std::string> flags; /** * @brief * Help string for this option. */ std::string help; /** * @brief * Number of arguments this option requires. Must be 0 or 1. All other values * have undefined behavior. Okay, the code actually works with positive * values in general, but it's unorthodox command line behavior. */ unsigned int num_args; /** * @brief * Returns true if this option does not want any arguments. */ bool wants_no_arguments() const; /** * @brief * Returns true if this option requires arguments. */ bool requires_arguments() const; }; /** * @brief * Checks whether or not a command line argument should be processed as an * option flag. This is very similar to is_valid_flag_definition() but must * allow for short flag groups (e.g. "-abc") and equal-assigned long flag * arguments (e.g. "--output=foo.txt"). */ bool cmd_line_arg_is_option_flag( const char* s); /** * @brief * Checks whether a flag in an option definition is valid. I suggest reading * through the function source to understand what dictates a valid. */ bool is_valid_flag_definition( const char* s); /** * @brief * Tests whether or not a valid flag is short. Assumes the provided cstring is * already a valid flag. */ bool flag_is_short( const char* s); /** * @brief * Contains two maps which aid in option parsing. The first map, @ref * short_map, maps from a short flag (just a character) to a pointer to the * original @ref definition that the flag represents. The second map, @ref * long_map, maps from a long flag (an std::string) to a pointer to the * original @ref definition that the flag represents. * * This object is usually a temporary that only exists during the parsing * operation. It is typically constructed using @ref validate_definitions(). */ struct parser_map { /** * @brief * Maps from a short flag (just a character) to a pointer to the original * @ref definition that the flag represents. */ std::array<const definition*, 256> short_map; /** * @brief * Maps from a long flag (an std::string) to a pointer to the original @ref * definition that the flag represents. */ std::unordered_map<std::string, const definition*> long_map; /** * @brief * Returns true if the provided short flag exists in the map object. */ bool known_short_flag( const char flag) const; /** * @brief * If the short flag exists in the map object then it is returned by this * method. If it doesn't then nullptr will be returned. */ const definition* get_definition_for_short_flag( const char flag) const; /** * @brief * Returns true if the provided long flag exists in the map object. */ bool known_long_flag( const std::string& flag) const; /** * @brief * If the long flag exists in the map object then it is returned by this * method. If it doesn't then nullptr will be returned. */ const definition* get_definition_for_long_flag( const std::string& flag) const; }; /** * @brief * Validates a collection (specifically an std::vector) of @ref definition * objects by checking if the contained flags are valid. If the set of @ref * definition objects is not valid then an exception is thrown. Upon successful * validation a @ref parser_map object is returned. */ parser_map validate_definitions( const std::vector<definition>& definitions); /** * @brief * A list of option definitions used to inform how to parse arguments. */ struct parser { /** * @brief * Vector of the option definitions which inform this parser how to parse * the command line arguments. */ std::vector<definition> definitions; /** * @brief * Parses the provided command line arguments and returns the results as * @ref parser_results. * * @note * This method is not thread-safe and assumes that no modifications are made * to the definitions member field during the extent of this method call. */ parser_results parse(int argc, const char** argv) const; /** * @brief * Through strict interpretation of pointer casting rules, despite this being * a safe operation, C++ doesn't allow implicit casts from <tt>char**</tt> to * <tt>const char**</tt> so here's an overload that performs a const_cast, * which is typically frowned upon but is safe here. */ parser_results parse(int argc, char** argv) const; }; /** * @brief * A convenience output stream that will accumulate what is streamed to it and * then, on destruction, format the accumulated string (via the * argagg::fmt_string() function) to the provided std::ostream. * * Example use: * * @code * { * argagg::fmt_ostream f(std::cerr); * f << "Usage: " << really_long_string << '\n'; * } // on destruction here the formatted string will be streamed to std::cerr * @endcode */ struct fmt_ostream : public std::ostringstream { /** * @brief * Reference to the final output stream that the formatted string will be * streamed to. */ std::ostream& output; /** * @brief * Construct to output to the provided output stream when this object is * destroyed. */ fmt_ostream(std::ostream& output); /** * @brief * Special destructor that will format the accumulated string using fmt (via * the argagg::fmt_string() function) and stream it to the std::ostream * stored. */ ~fmt_ostream(); }; /** * @brief * Processes the provided string using the fmt utility and returns the * resulting output as a string. Not the most efficient (in time or space) but * gets the job done. */ std::string fmt_string(const std::string& s); } // namespace argagg /** * @brief * Writes the option help to the given stream. */ std::ostream& operator << (std::ostream& os, const argagg::parser& x); // ---- end of declarations, header-only implementations follow ---- namespace argagg { template <typename T> T option_result::as() const { if (this->arg) { return convert::arg<T>(this->arg); } else { throw option_lacks_argument_error("option has no argument"); } } template <typename T> T option_result::as(const T& t) const { if (this->arg) { try { return convert::arg<T>(this->arg); } catch (...) { return t; } } else { // I actually think this will never happen. To call this method you have // to access a specific option_result for an option. If there's a // specific option_result then the option was found. If the option // requires an argument then it will definitely have an argument // otherwise the parser would have complained. return t; } } template <typename T> option_result::operator T () const { return this->as<T>(); } template <> inline option_result::operator bool () const { return this->arg != nullptr; } inline bool option_result::operator ! () const { return !static_cast<bool>(*this); } inline std::size_t option_results::count() const { return this->all.size(); } inline option_result& option_results::operator [] (std::size_t index) { return this->all[index]; } inline const option_result& option_results::operator [] (std::size_t index) const { return this->all[index]; } template <typename T> T option_results::as() const { if (this->all.size() == 0) { throw std::out_of_range("no option arguments to convert"); } return this->all.back().as<T>(); } template <typename T> T option_results::as(const T& t) const { if (this->all.size() == 0) { return t; } return this->all.back().as<T>(t); } template <typename T> option_results::operator T () const { return this->as<T>(); } template <> inline option_results::operator bool () const { return this->all.size() > 0; } inline bool option_results::operator ! () const { return !static_cast<bool>(*this); } inline bool parser_results::has_option(const std::string& name) const { const auto it = this->options.find(name); return ( it != this->options.end()) && it->second.all.size() > 0; } inline option_results& parser_results::operator [] (const std::string& name) try { return this->options.at(name); } catch (const std::out_of_range& e) { std::ostringstream msg; msg << "no option named \"" << name << "\" in parser_results"; throw unknown_option(msg.str()); } inline const option_results& parser_results::operator [] (const std::string& name) const try { return this->options.at(name); } catch (const std::out_of_range& e) { std::ostringstream msg; msg << "no option named \"" << name << "\" in parser_results"; throw unknown_option(msg.str()); } inline std::size_t parser_results::count() const { return this->pos.size(); } inline const char* parser_results::operator [] (std::size_t index) const { return this->pos[index]; } template <typename T> T parser_results::as(std::size_t i) const { return convert::arg<T>(this->pos[i]); } template <typename T> std::vector<T> parser_results::all_as() const { std::vector<T> v(this->pos.size()); std::transform( this->pos.begin(), this->pos.end(), v.begin(), [](const char* arg) { return convert::arg<T>(arg); }); return v; } inline bool definition::wants_no_arguments() const { return this->num_args == 0; } inline bool definition::requires_arguments() const { return this->num_args > 0; } inline bool cmd_line_arg_is_option_flag( const char* s) { auto len = std::strlen(s); // The shortest possible flag has two characters: a hyphen and an // alpha-numeric character. if (len < 2) { return false; } // All flags must start with a hyphen. if (s[0] != '-') { return false; } // Shift the name forward by a character to account for the initial hyphen. // This means if s was originally "-v" then name will be "v". const char* name = s + 1; // Check if we're dealing with a long flag. bool is_long = false; if (s[1] == '-') { is_long = true; // Just -- is not a valid flag. if (len == 2) { return false; } // Shift the name forward to account for the extra hyphen. This means if s // was originally "--output" then name will be "output". name = s + 2; } // The first character of the flag name must be alpha-numeric. This is to // prevent things like "---a" from being valid flags. len = std::strlen(name); if (!std::isalnum(name[0])) { return false; } // At this point in is_valid_flag_definition() we would check if the short // flag has only one character. At command line specification you can group // short flags together or even add an argument to a short flag without a // space delimiter. Thus we don't check if this has only one character // because it might not. // If this is a long flag then we expect all characters *up to* an equal sign // to be alpha-numeric or a hyphen. After the equal sign you are specify the // argument to a long flag which can be basically anything. if (is_long) { bool encountered_equal = false; return std::all_of(name, name + len, [&](const char& c) { if (encountered_equal) { return true; } else { if (c == '=') { encountered_equal = true; return true; } return std::isalnum(c) || c == '-'; } }); } // At this point we are not dealing with a long flag. We already checked that // the first character is alpha-numeric so we've got the case of a single // short flag covered. This might be a short flag group though and we might // be tempted to check that each character of the short flag group is // alpha-numeric. However, you can specify the argument for a short flag // without a space delimiter (e.g. "-I/usr/local/include") so you can't tell // if the rest of a short flag group is part of the argument or not unless // you know what is a defined flag or not. We leave that kind of processing // to the parser. return true; } inline bool is_valid_flag_definition( const char* s) { auto len = std::strlen(s); // The shortest possible flag has two characters: a hyphen and an // alpha-numeric character. if (len < 2) { return false; } // All flags must start with a hyphen. if (s[0] != '-') { return false; } // Shift the name forward by a character to account for the initial hyphen. // This means if s was originally "-v" then name will be "v". const char* name = s + 1; // Check if we're dealing with a long flag. bool is_long = false; if (s[1] == '-') { is_long = true; // Just -- is not a valid flag. if (len == 2) { return false; } // Shift the name forward to account for the extra hyphen. This means if s // was originally "--output" then name will be "output". name = s + 2; } // The first character of the flag name must be alpha-numeric. This is to // prevent things like "---a" from being valid flags. len = std::strlen(name); if (!std::isalnum(name[0])) { return false; } // If this is a short flag then it must only have one character. if (!is_long && len > 1) { return false; } // The rest of the characters must be alpha-numeric, but long flags are // allowed to have hyphens too. return std::all_of(name + 1, name + len, [&](const char& c) { return std::isalnum(c) || (c == '-' && is_long); }); } inline bool flag_is_short( const char* s) { return s[0] == '-' && std::isalnum(s[1]); } inline bool parser_map::known_short_flag( const char flag) const { return this->short_map[static_cast<std::size_t>(flag)] != nullptr; } inline const definition* parser_map::get_definition_for_short_flag( const char flag) const { return this->short_map[static_cast<std::size_t>(flag)]; } inline bool parser_map::known_long_flag( const std::string& flag) const { const auto existing_long_flag = this->long_map.find(flag); return existing_long_flag != long_map.end(); } inline const definition* parser_map::get_definition_for_long_flag( const std::string& flag) const { const auto existing_long_flag = this->long_map.find(flag); if (existing_long_flag == long_map.end()) { return nullptr; } return existing_long_flag->second; } inline parser_map validate_definitions( const std::vector<definition>& definitions) { std::unordered_map<std::string, const definition*> long_map; parser_map map {{{nullptr}}, std::move(long_map)}; for (auto& defn : definitions) { if (defn.flags.size() == 0) { std::ostringstream msg; msg << "option \"" << defn.name << "\" has no flag definitions"; throw invalid_flag(msg.str()); } for (auto& flag : defn.flags) { if (!is_valid_flag_definition(flag.data())) { std::ostringstream msg; msg << "flag \"" << flag << "\" specified for option \"" << defn.name << "\" is invalid"; throw invalid_flag(msg.str()); } if (flag_is_short(flag.data())) { const std::size_t short_flag_letter = static_cast<std::size_t>(flag[1]); const auto existing_short_flag = map.short_map[short_flag_letter]; bool short_flag_already_exists = (existing_short_flag != nullptr); if (short_flag_already_exists) { std::ostringstream msg; msg << "duplicate short flag \"" << flag << "\" found, specified by both option \"" << defn.name << "\" and option \"" << existing_short_flag->name; throw invalid_flag(msg.str()); } map.short_map[static_cast<std::size_t>(short_flag_letter)] = &defn; continue; } // If we're here then this is a valid, long-style flag. if (map.known_long_flag(flag)) { const auto existing_long_flag = map.get_definition_for_long_flag(flag); std::ostringstream msg; msg << "duplicate long flag \"" << flag << "\" found, specified by both option \"" << defn.name << "\" and option \"" << existing_long_flag->name; throw invalid_flag(msg.str()); } map.long_map.insert(std::make_pair(flag, &defn)); } } return map; } inline parser_results parser::parse(int argc, const char** argv) const { // Inspect each definition to see if its valid. You may wonder "why don't // you do this validation on construction?" I had thought about it but // realized that since I've made the parser an aggregate type (granted it // just "aggregates" a single vector) I would need to track any changes to // the definitions vector and re-run the validity check in order to // maintain this expected "validity invariant" on the object. That would // then require hiding the definitions vector as a private entry and then // turning the parser into a thin interface (by re-exposing setters and // getters) to the vector methods just so that I can catch when the // definition has been modified. It seems much simpler to just enforce the // validity when you actually want to parse because it's at the moment of // parsing that you know the definitions are complete. parser_map map = validate_definitions(this->definitions); // Initialize the parser results that we'll be returning. Store the program // name (assumed to be the first command line argument) and initialize // everything else as empty. std::unordered_map<std::string, option_results> options {}; std::vector<const char*> pos; parser_results results {argv[0], std::move(options), std::move(pos)}; // Add an empty option result for each definition. for (const auto& defn : this->definitions) { option_results opt_results {{}}; results.options.insert( std::make_pair(defn.name, opt_results)); } // Don't start off ignoring flags. We only ignore flags after a -- shows up // in the command line arguments. bool ignore_flags = false; // Keep track of any options that are expecting arguments. const char* last_flag_expecting_args = nullptr; option_result* last_option_expecting_args = nullptr; unsigned int num_option_args_to_consume = 0; // Get pointers to pointers so we can treat the raw pointer array as an // iterator for standard library algorithms. This isn't used yet but can be // used to template this function to work on iterators over strings or // C-strings. const char** arg_i = argv + 1; const char** arg_end = argv + argc; while (arg_i != arg_end) { auto arg_i_cstr = *arg_i; auto arg_i_len = std::strlen(arg_i_cstr); // Some behavior to note: if the previous option is expecting an argument // then the next entry will be treated as a positional argument even if // it looks like a flag. bool treat_as_positional_argument = ( ignore_flags || num_option_args_to_consume > 0 || !cmd_line_arg_is_option_flag(arg_i_cstr) ); if (treat_as_positional_argument) { // If last option is expecting some specific positive number of // arguments then give this argument to that option, *regardless of // whether or not the argument looks like a flag or is the special "--" // argument*. if (num_option_args_to_consume > 0) { last_option_expecting_args->arg = arg_i_cstr; --num_option_args_to_consume; ++arg_i; continue; } // Now we check if this is just "--" which is a special argument that // causes all following arguments to be treated as non-options and is // itselve discarded. if (std::strncmp(arg_i_cstr, "--", 2) == 0 && arg_i_len == 2) { ignore_flags = true; ++arg_i; continue; } // If there are no expectations for option arguments then simply use // this argument as a positional argument. results.pos.push_back(arg_i_cstr); ++arg_i; continue; } // Reset the "expecting argument" state. last_flag_expecting_args = nullptr; last_option_expecting_args = nullptr; num_option_args_to_consume = 0; // If we're at this point then we're definitely dealing with something // that is flag-like and has hyphen as the first character and has a // length of at least two characters. How we handle this potential flag // depends on whether or not it is a long-option so we check that first. bool is_long_flag = (arg_i_cstr[1] == '-'); if (is_long_flag) { // Long flags have a complication: their arguments can be specified // using an '=' character right inside the argument. That means an // argument like "--output=foobar.txt" is actually an option with flag // "--output" and argument "foobar.txt". So we look for the first // instance of the '=' character and keep it in long_flag_arg. If // long_flag_arg is nullptr then we didn't find '='. We need the // flag_len to construct long_flag_str below. auto long_flag_arg = std::strchr(arg_i_cstr, '='); std::size_t flag_len = arg_i_len; if (long_flag_arg != nullptr) { flag_len = static_cast<std::size_t>(long_flag_arg - arg_i_cstr); } std::string long_flag_str(arg_i_cstr, flag_len); if (!map.known_long_flag(long_flag_str)) { std::ostringstream msg; msg << "found unexpected flag: " << long_flag_str; throw unexpected_option_error(msg.str()); } const auto defn = map.get_definition_for_long_flag(long_flag_str); if (long_flag_arg != nullptr && defn->num_args == 0) { std::ostringstream msg; msg << "found argument for option not expecting an argument: " << arg_i_cstr; throw unexpected_argument_error(msg.str()); } // We've got a legitimate, known long flag option so we add an option // result. This option result initially has an arg of nullptr, but that // might change in the following block. auto& opt_results = results.options[defn->name]; option_result opt_result {nullptr}; opt_results.all.push_back(std::move(opt_result)); if (defn->requires_arguments()) { bool there_is_an_equal_delimited_arg = (long_flag_arg != nullptr); if (there_is_an_equal_delimited_arg) { // long_flag_arg would be "=foo" in the "--output=foo" case so we // increment by 1 to get rid of the equal sign. opt_results.all.back().arg = long_flag_arg + 1; } else { last_flag_expecting_args = arg_i_cstr; last_option_expecting_args = &(opt_results.all.back()); num_option_args_to_consume = defn->num_args; } } ++arg_i; continue; } // If we've made it here then we're looking at either a short flag or a // group of short flags. Short flags can be grouped together so long as // they don't require any arguments unless the option that does is the // last in the group ("-o x -v" is okay, "-vo x" is okay, "-ov x" is // not). So starting after the dash we're going to process each character // as if it were a separate flag. Note "sf_idx" stands for "short flag // index". for (std::size_t sf_idx = 1; sf_idx < arg_i_len; ++sf_idx) { const auto short_flag = arg_i_cstr[sf_idx]; if (!std::isalnum(short_flag)) { std::ostringstream msg; msg << "found non-alphanumeric character '" << arg_i_cstr[sf_idx] << "' in flag group '" << arg_i_cstr << "'"; throw std::domain_error(msg.str()); } if (!map.known_short_flag(short_flag)) { std::ostringstream msg; msg << "found unexpected flag '" << arg_i_cstr[sf_idx] << "' in flag group '" << arg_i_cstr << "'"; throw unexpected_option_error(msg.str()); } auto defn = map.get_definition_for_short_flag(short_flag); auto& opt_results = results.options[defn->name]; // Create an option result with an empty argument (for now) and add it // to this option's results. option_result opt_result {nullptr}; opt_results.all.push_back(std::move(opt_result)); if (defn->requires_arguments()) { // If this short flag's option requires an argument and we're the // last flag in the short flag group then just put the parser into // "expecting argument for last option" state and move onto the next // command line argument. bool is_last_short_flag_in_group = (sf_idx == arg_i_len - 1); if (is_last_short_flag_in_group) { last_flag_expecting_args = arg_i_cstr; last_option_expecting_args = &(opt_results.all.back()); num_option_args_to_consume = defn->num_args; break; } // If this short flag's option requires an argument and we're NOT the // last flag in the short flag group then we automatically consume // the rest of the short flag group as the argument for this flag. // This is how we get the POSIX behavior of being able to specify a // flag's arguments without a white space delimiter (e.g. // "-I/usr/local/include"). opt_results.all.back().arg = arg_i_cstr + sf_idx + 1; break; } } ++arg_i; continue; } // If we're done with all of the arguments but are still expecting // arguments for a previous option then we haven't satisfied that option. // This is an error. if (num_option_args_to_consume > 0) { std::ostringstream msg; msg << "last option \"" << last_flag_expecting_args << "\" expects an argument but the parser ran out of command line " << "arguments to parse"; throw option_lacks_argument_error(msg.str()); } return results; } inline parser_results parser::parse(int argc, char** argv) const { return parse(argc, const_cast<const char**>(argv)); } namespace convert { /** * @brief * Templated function for conversion to T using the @ref std::strtol() * function. This is used for anything long length or shorter (long, int, * short, char). */ template <typename T> inline T long_(const char* arg) { char* endptr = nullptr; errno = 0; T ret = static_cast<T>(std::strtol(arg, &endptr, 0)); if (endptr == arg) { std::ostringstream msg; msg << "unable to convert argument to integer: \"" << arg << "\""; throw std::invalid_argument(msg.str()); } if (errno == ERANGE) { throw std::out_of_range("argument numeric value out of range"); } return ret; } /** * @brief * Templated function for conversion to T using the @ref std::strtoll() * function. This is used for anything long long length or shorter (long * long). */ template <typename T> inline T long_long_(const char* arg) { char* endptr = nullptr; errno = 0; T ret = static_cast<T>(std::strtoll(arg, &endptr, 0)); if (endptr == arg) { std::ostringstream msg; msg << "unable to convert argument to integer: \"" << arg << "\""; throw std::invalid_argument(msg.str()); } if (errno == ERANGE) { throw std::out_of_range("argument numeric value out of range"); } return ret; } #define DEFINE_CONVERSION_FROM_LONG_(TYPE) \ template <> inline \ TYPE arg(const char* arg) \ { \ return long_<TYPE>(arg); \ } DEFINE_CONVERSION_FROM_LONG_(char) DEFINE_CONVERSION_FROM_LONG_(unsigned char) DEFINE_CONVERSION_FROM_LONG_(signed char) DEFINE_CONVERSION_FROM_LONG_(short) DEFINE_CONVERSION_FROM_LONG_(unsigned short) DEFINE_CONVERSION_FROM_LONG_(int) DEFINE_CONVERSION_FROM_LONG_(unsigned int) DEFINE_CONVERSION_FROM_LONG_(long) DEFINE_CONVERSION_FROM_LONG_(unsigned long) #undef DEFINE_CONVERSION_FROM_LONG_ #define DEFINE_CONVERSION_FROM_LONG_LONG_(TYPE) \ template <> inline \ TYPE arg(const char* arg) \ { \ return long_long_<TYPE>(arg); \ } DEFINE_CONVERSION_FROM_LONG_LONG_(long long) DEFINE_CONVERSION_FROM_LONG_LONG_(unsigned long long) #undef DEFINE_CONVERSION_FROM_LONG_LONG_ template <typename T> T arg(const char* arg) { return converter<T>::convert(arg); } template <> inline bool arg(const char* arg) { return argagg::convert::arg<int>(arg) != 0; } template <> inline float arg(const char* arg) { char* endptr = nullptr; errno = 0; float ret = std::strtof(arg, &endptr); if (endptr == arg) { std::ostringstream msg; msg << "unable to convert argument to integer: \"" << arg << "\""; throw std::invalid_argument(msg.str()); } if (errno == ERANGE) { throw std::out_of_range("argument numeric value out of range"); } return ret; } template <> inline double arg(const char* arg) { char* endptr = nullptr; errno = 0; double ret = std::strtod(arg, &endptr); if (endptr == arg) { std::ostringstream msg; msg << "unable to convert argument to integer: \"" << arg << "\""; throw std::invalid_argument(msg.str()); } if (errno == ERANGE) { throw std::out_of_range("argument numeric value out of range"); } return ret; } template <> inline const char* arg(const char* arg) { return arg; } template <> inline std::string arg(const char* arg) { return std::string(arg); } template <typename T> bool parse_next_component( const char*& s, T& out_arg, const char delim) { const char* begin = s; s = std::strchr(s, delim); if (s == nullptr) { std::string arg_str(begin); out_arg = argagg::convert::arg<T>(arg_str.c_str()); return false; } else { std::string arg_str(begin, static_cast<std::size_t>(s - begin)); out_arg = argagg::convert::arg<T>(arg_str.c_str()); s += 1; return true; } } } // namespace convert inline fmt_ostream::fmt_ostream(std::ostream& output) : std::ostringstream(), output(output) { } inline fmt_ostream::~fmt_ostream() { output << fmt_string(this->str()); } inline std::string lstrip(const std::string& text) { auto result = text; result.erase( result.begin(), std::find_if( result.begin(), result.end(), [](int ch) { return !std::isspace(ch); })); return result; } inline std::string rstrip(const std::string& text) { auto result = text; result.erase( std::find_if( result.rbegin(), result.rend(), [](int ch) { return !std::isspace(ch); }).base(), result.end()); return result; } inline std::string construct_line(const std::string& indent, const std::string& contents) { return indent + rstrip(contents) + "\n"; } /** * @brief * Return a wrapped version of a single line of text. */ inline std::string wrap_line(const std::string& single_line, const std::size_t wrap_width) { auto indentation_spaces = single_line.find_first_not_of(" "); if (indentation_spaces == std::string::npos) { indentation_spaces = 0; } const auto line = lstrip(single_line); const auto indent = std::string(indentation_spaces, ' '); std::string result; std::size_t position = 0; std::size_t line_start = 0; while (true) { const auto new_position = line.find_first_of(" ", position); if (new_position == std::string::npos) { break; } if (new_position + indentation_spaces > line_start + wrap_width) { result += construct_line( indent, line.substr(line_start, position - line_start - 1)); line_start = position; } position = new_position + 1; } return result + construct_line(indent, line.substr(line_start)); } inline std::string fmt_string(const std::string& s) { std::stringstream ss(s); std::string line; std::string result; // Use default width of `fmt`. const auto column_width = 75; while (std::getline(ss, line, '\n')) { result += wrap_line(line, column_width); } return result; } } // namespace argagg inline std::ostream& operator << (std::ostream& os, const argagg::parser& x) { for (auto& definition : x.definitions) { os << " "; for (auto& flag : definition.flags) { os << flag; if (flag != definition.flags.back()) { os << ", "; } } os << "\n " << definition.help << '\n'; } return os; } #endif // ARGAGG_ARGAGG_ARGAGG_HPP
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor