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