Mercurial > code
changeset 413:52a13aad3d02
OptionParser: complete rewrite, simpler
author | David Demelier <markand@malikania.fr> |
---|---|
date | Wed, 07 Oct 2015 14:25:09 +0200 |
parents | f16164e720ba |
children | 0b6f7f7c8a6c |
files | C++/modules/OptionParser/OptionParser.cpp C++/modules/OptionParser/OptionParser.h |
diffstat | 2 files changed, 199 insertions(+), 355 deletions(-) [+] |
line wrap: on
line diff
--- a/C++/modules/OptionParser/OptionParser.cpp Wed Oct 07 08:36:22 2015 +0200 +++ b/C++/modules/OptionParser/OptionParser.cpp Wed Oct 07 14:25:09 2015 +0200 @@ -1,7 +1,7 @@ /* - * OptionParser.cpp -- command line option parser + * OptionParser.cpp -- parse Unix command line options * - * Copyright (c) 2013-2015 David Demelier <markand@malikania.fr> + * Copyright (c) 2015 David Demelier <markand@malikania.fr> * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -16,196 +16,174 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -#include <algorithm> +#include <cassert> #include "OptionParser.h" -bool OptionParser::isShort(const std::string &arg) const +namespace parser { + +namespace { + +using Iterator = std::vector<std::string>::iterator; +using Args = std::vector<std::string>; + +inline bool isOption(const std::string &arg) noexcept { - return arg.size() >= 2 && arg[0] == '-' && arg[1] != '-'; + return arg.size() >= 2 && arg[0] == '-'; } -bool OptionParser::isLong(const std::string &arg) const +inline bool isLongOption(const std::string &arg) noexcept { - return arg.size() >= 3 && arg[0] == '-' && arg[1] == '-' && arg[2] != '-'; -} + assert(isOption(arg)); -bool OptionParser::isOption(const std::string &arg) const -{ - return isShort(arg) || isLong(arg); + return arg.size() >= 3 && arg[1] == '-'; } -std::string OptionParser::key(const std::string &arg) const +inline bool isShortSimple(const std::string &arg) noexcept { - if (isShort(arg)) - return arg.substr(1, 1); + assert(isOption(arg)); + assert(!isLongOption(arg)); - return arg.substr(2); + return arg.size() == 2; } -bool OptionParser::isShortCompacted(const std::string &arg) const -{ - return arg.size() >= 3; -} - -bool OptionParser::isDefined(const std::string &arg) const +void parseLongOption(Result &result, Args &args, Iterator &it, Iterator &end, const Options &definition) { - auto n = key(arg); - auto it = std::find_if(m_options.begin(), m_options.end(), [&] (const Option &o) -> bool { - return o.key() == n || o.full() == n; - }); + auto arg = *it++; + auto opt = definition.find(arg); + + if (opt == definition.end()) { + throw InvalidOption{arg}; + } - return it != m_options.end(); + /* Need argument? */ + if (opt->second) { + if (it == end || isOption(*it)) { + throw MissingValue{arg}; + } + + result.insert(std::make_pair(arg, *it++)); + it = args.erase(args.begin(), it); + end = args.end(); + } else { + result.insert(std::make_pair(arg, "")); + it = args.erase(args.begin()); + end = args.end(); + } } -const Option &OptionParser::get(const std::string &arg) const -{ - std::string n = key(arg); - - return *std::find_if(m_options.begin(), m_options.end(), [&] (const Option &o) -> bool { - return o.key() == n || o.full() == n; - }); -} - -bool OptionParser::isToggle(const std::string &arg) const -{ - return (get(arg).flags() & Option::NoArg); -} - -void OptionParser::readShort(OptionPack &pack, Args::const_iterator &it, Args::const_iterator end) const +void parseShortOption(Result &result, Args &args, Iterator &it, Iterator &end, const Options &definition) { - /* - * There are many options when passing a short option: - * - * 1. -cmyconfig Takes on argument but parsed as unique, - * 2. -c myconfig Takes on argument but parsed as two strings - * 3. -abc If a is not a toggle option, its argument is `bc' - * 4. -abc If a is a toggle option and b, c are toggle, they are added - */ + if (isShortSimple(*it)) { + /* + * Here two cases: + * + * -v (no option) + * -c value + */ + auto arg = *it++; + auto opt = definition.find(arg); - std::string v = it->substr(2); - std::string k = key(*it); - const Option &option = get(std::string("-") + k); + if (opt == definition.end()) { + throw InvalidOption{arg}; + } + + /* Need argument? */ + if (opt->second) { + if (it == end || isOption(*it)) { + throw MissingValue{arg}; + } - if (isToggle(*it)) { - // 3. and optionally 4. - pack.push_back(OptionValue(option, "")); - pack.m_argsParsed += 1; + result.insert(std::make_pair(arg, *it++)); + it = args.erase(args.begin(), it); + end = args.end(); + } else { + result.insert(std::make_pair(arg, "")); + it = args.erase(args.begin()); + end = args.end(); + } + } else { + /* + * Here multiple scenarios: + * + * 1. -abc (-a -b -c if all are simple boolean arguments) + * 2. -vc foo.conf (-v -c foo.conf if -c is argument dependant) + * 3. -vcfoo.conf (-v -c foo.conf also) + */ + auto value = it->substr(1); + auto len = value.length(); + int toremove = 1; - if (isShortCompacted(*it)) { - for (char c : v) { - if (!isDefined("-" + std::string(1, c))) { - pack.m_error = "-" + std::string(1, c) + " is not a valid option"; - break; + for (decltype(len) i = 0; i < len; ++i) { + auto arg = std::string{'-'} + value[i]; + auto opt = definition.find(arg); + + if (opt == definition.end()) { + throw InvalidOption{arg}; + } + + if (opt->second) { + if (i == (len - 1)) { + /* End of string, get the next argument (see 2.) */ + if (++it == end || isOption(*it)) { + throw MissingValue{arg}; + } + + result.insert(std::make_pair(arg, *it)); + toremove += 1; + } else { + result.insert(std::make_pair(arg, value.substr(i + 1))); + i = len; } - - const Option &sub = get("-" + std::string(1, c)); - - pack.push_back(OptionValue(sub, "")); + } else { + result.insert(std::make_pair(arg, "")); } } - ++ it; - } else { - // 1. - if (isShortCompacted(*it++)) { - pack.push_back(OptionValue(option, v)); - pack.m_argsParsed += 1; - } else { - // 2. - if (it == end) { - pack.m_error = option.key() + " requires an option"; - } else { - pack.push_back(OptionValue(option, *it++)); - pack.m_argsParsed += 2; - } - } - } -} - -void OptionParser::readFull(OptionPack &pack, Args::const_iterator &it, Args::const_iterator end) const -{ - /* - * Long options can't be compacted, there are only two possibilities: - * - * 1. --fullscreen No argument - * 2. --config foo One argument - */ - const Option &option = get(*it); - - if (!isToggle(*it)) { - // 2. - if (++it == end) { - pack.m_error = "--" + option.full() + " requires an option"; - } else { - pack.push_back(OptionValue(option, *it++)); - pack.m_argsParsed += 2; - } - } else { - pack.push_back(OptionValue(option, "")); - pack.m_argsParsed ++; - - ++ it; + it = args.erase(args.begin(), args.begin() + toremove); + end = args.end(); } } -OptionParser::OptionParser(std::initializer_list<Option> options) - : m_options(options.begin(), options.end()) -{ -} +} // !namespace -OptionParser::OptionParser(std::vector<Option> options) - : m_options(std::move(options)) +Result read(std::vector<std::string> &args, const Options &definition) { -} + Result result; -OptionPack OptionParser::parse(Args::const_iterator it, Args::const_iterator end, int flags) const -{ - OptionPack pack; + auto it = args.begin(); + auto end = args.end(); while (it != end) { if (!isOption(*it)) { - if (flags & Unstrict) { - pack.m_argsParsed ++; - it ++; - continue; - } else { - pack.m_error = *it + " is not an option"; - return pack; - } + break; } - if (!isDefined(*it)) { - pack.m_error = *it + ": invalid option"; - return pack; - } - - if (isShort(*it)) { - readShort(pack, it, end); + if (isLongOption(*it)) { + parseLongOption(result, args, it, end, definition); } else { - readFull(pack, it, end); - } - - // Read failure - if (pack.m_error != "No error") { - return pack; + parseShortOption(result, args, it, end, definition); } } - return pack; + return result; } -OptionPack OptionParser::parse(int argc, char **argv, int flags) const +Result read(int &argc, char **&argv, const Options &definition) { std::vector<std::string> args; - for (int i = 0; i < argc; ++i) + for (int i = 0; i < argc; ++i) { args.push_back(argv[i]); + } - return parse(args, flags); + auto before = args.size(); + auto result = read(args, definition); + + argc -= before - args.size(); + argv += before - args.size(); + + return result; } -OptionPack OptionParser::parse(const std::vector<std::string> &args, int flags) const -{ - return parse(args.begin(), args.end(), flags); -} +} // !parser \ No newline at end of file
--- a/C++/modules/OptionParser/OptionParser.h Wed Oct 07 08:36:22 2015 +0200 +++ b/C++/modules/OptionParser/OptionParser.h Wed Oct 07 14:25:09 2015 +0200 @@ -1,7 +1,7 @@ /* - * OptionParser.h -- command line option parser + * OptionParser.h -- parse Unix command line options * - * Copyright (c) 2013-2015 David Demelier <markand@malikania.fr> + * Copyright (c) 2015 David Demelier <markand@malikania.fr> * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -19,257 +19,123 @@ #ifndef _OPTION_PARSER_H_ #define _OPTION_PARSER_H_ -/** - * @file OptionParser.h - * @brief Command line option parser - */ - -#include <initializer_list> +#include <exception> +#include <map> #include <string> +#include <utility> #include <vector> /** - * @class Option - * @brief Option definition + * Namespace for options parsing. */ -class Option { -public: - enum Flags { - NoArg = (1 << 0), - }; +namespace parser { +/** + * @class InvalidOption + * @brief This exception is thrown when an invalid option has been found. + */ +class InvalidOption : public std::exception { private: - std::string m_key; - std::string m_full; - int m_flags; + std::string message; public: /** - * Construct an option. By default, an option requires an argument - * unless flags is set to NoArg. - * - * You <strong>must</strong> not prepend dashes to the option names. - * - * You don't need to set both short and long names, but you need at - * least one. + * The invalid option given. + */ + std::string argument; + + /** + * Construct the exception. * - * @param key the short name (e.g v) - * @param full the long name (e.g verbose) - * @param flags the optional flags - * @see Flags + * @param arg the argument missing */ - inline Option(std::string key, std::string full, int flags = 0) - : m_key(std::move(key)) - , m_full(std::move(full)) - , m_flags(flags) + inline InvalidOption(std::string arg) + : argument{std::move(arg)} { + message = std::string{"invalid option: "} + argument; } /** - * Get the short name (e.g v) + * Get the error message. * - * @return the short name - */ - inline const std::string &key() const noexcept - { - return m_key; - } - - /** - * Get the long name (e.g verbose) - * - * @return the long name + * @return the error message */ - inline const std::string &full() const noexcept + const char *what() const noexcept override { - return m_full; - } - - /** - * Get the flags. - * - * @return the flags - * @see Flags - */ - inline int flags() const noexcept - { - return m_flags; + return message.c_str(); } }; /** - * @class OptionValue - * @brief Result of an option parse + * @class MissingValue + * @brief This exception is thrown when an option requires a value and no value has been given */ -class OptionValue { +class MissingValue : public std::exception { private: - std::string m_key; - std::string m_full; - std::string m_value; + std::string message; public: /** - * Construct an option value - * - * @param option the option - * @param value the value + * The argument that requires a value. */ - inline OptionValue(const Option &option, std::string value) - : m_key(option.key()) - , m_full(option.full()) - , m_value(std::move(value)) + std::string argument; + + /** + * Construct the exception. + * + * @param arg the argument that requires a value + */ + inline MissingValue(std::string arg) + : argument{std::move(arg)} { + message = std::string{"missing argument for: "} + argument; } /** - * Get the value (if the option requires an argument). + * Get the error message. * - * @return the value + * @return the error message */ - inline const std::string &value() const noexcept + const char *what() const noexcept override { - return m_value; - } - - friend bool operator==(const OptionValue &o1, const std::string &name); -}; - -/** - * Test the option value with the specified option name. - * - * You can use both the short option or the long option name depending - * on what you have registered to the OptionParser class. - * - * @param o the option - * @param name the short or the full name - * @return true if matches - */ -inline bool operator==(const OptionValue &o, const std::string &name) -{ - return o.m_key == name || o.m_full == name; -} - -/** - * @class OptionPack - * @brief Object containing results of a parse - * - * Because parsing bad options does not throw exceptions, this class is - * convertible to bool and has the error contained. - * - * It also have the number of arguments parsed so you can cut your options - * depending on the full command line. - * - * Example: - * -y install -d foo - * -y remove -f - * - * In that case, you can do two parsing, it will stops (unless Unstrict is set) - * until install or remove. - */ -class OptionPack : public std::vector<OptionValue> { -private: - friend class OptionParser; - - std::string m_error{"No error"}; - int m_argsParsed{0}; - -public: - /** - * Get the error. - * - * @return the error - */ - inline const std::string &error() const noexcept - { - return m_error; - } - - /** - * Get the number of arguments parsed <strong>not the number of - * options</strong>. - * - * @return the number of arguments parsed - */ - inline int parsed() const noexcept - { - return m_argsParsed; - } - - /** - * Convert to true on success. - * - * @return true on success - */ - inline operator bool() const noexcept - { - return m_error == "No error"; + return message.c_str(); } }; /** - * @class OptionParser - * @brief Base class for parsing command line options - * - * The option parser is a replacement for getopt(3) which is reentrant - * and does not use globals. + * Packed multimap of options. */ -class OptionParser { -public: - using Map = std::vector<Option>; - using Args = std::vector<std::string>; - - enum Flags { - Unstrict = (1 << 0) - }; +using Result = std::multimap<std::string, std::string>; -private: - Map m_options; - - const Option &get(const std::string &arg) const; - std::string key(const std::string &arg) const; - bool isDefined(const std::string &arg) const; - bool isToggle(const std::string &arg) const; - bool isShortCompacted(const std::string &arg) const; - bool isShort(const std::string &arg) const; - bool isLong(const std::string &arg) const; - bool isOption(const std::string &arg) const; - void readShort(OptionPack &pack, Args::const_iterator &it, Args::const_iterator end) const; - void readFull(OptionPack &pack, Args::const_iterator &it, Args::const_iterator end) const; - OptionPack parse(Args::const_iterator it, Args::const_iterator end, int flags) const; +/** + * Define the allowed options. + */ +using Options = std::map<std::string, bool>; -public: - /** - * Construct an option parser from an initializer_list of options. - * - * @param options the list of options - */ - OptionParser(std::initializer_list<Option> options); - - /** - * Construct an option parser from a vector of options. - * - * @param options the options - */ - OptionParser(std::vector<Option> options); +/** + * Extract the command line options and return a result. + * + * @param args the arguments + * @param definition + * @warning the arguments vector is modified in place to remove parsed options + * @throw MissingValue + * @throw InvalidOption + */ +Result read(std::vector<std::string> &args, const Options &definition); - /** - * Parse the arguments from main arguments. - * - * @param argc the number of arguments - * @param argv the arguments - * @param flags the optional flags - * @return the packed result - */ - OptionPack parse(int argc, char **argv, int flags = 0) const; +/** + * Overloaded function for usage with main() arguments. + * + * @param argc the number of arguments + * @param argv the argument vector + * @param definition + * @note don't forget to remove the first argv[0] argument + * @warning the argc and argv are modified in place to remove parsed options + * @throw MissingValue + * @throw InvalidOption + */ +Result read(int &argc, char **&argv, const Options &definition); - /** - * Parse the arguments from a vector. - * - * @param args the arguments - * @param flags the optional flags - * @return the packed result - */ - OptionPack parse(const std::vector<std::string> &args, int flags = 0) const; -}; +} // !parser -#endif // !_OPTION_PARSER_H_ +#endif // !_OPTION_PARSER_H_ \ No newline at end of file