changeset 657:11fa64b69530

options: reintroduce a very basic getopt(3) alternative
author David Demelier <markand@malikania.fr>
date Mon, 15 Jul 2019 13:54:17 +0200
parents 734ce3a26a58
children 868663a44b5e
files cpp/CMakeLists.txt cpp/options/CMakeLists.txt cpp/options/options.hpp cpp/options/test/main.cpp
diffstat 4 files changed, 356 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/cpp/CMakeLists.txt	Mon Jan 21 20:45:02 2019 +0100
+++ b/cpp/CMakeLists.txt	Mon Jul 15 13:54:17 2019 +0200
@@ -20,5 +20,6 @@
 add_subdirectory(is_number)
 add_subdirectory(join)
 add_subdirectory(json_util)
+add_subdirectory(options)
 add_subdirectory(pexec)
 add_subdirectory(to_int)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cpp/options/CMakeLists.txt	Mon Jul 15 13:54:17 2019 +0200
@@ -0,0 +1,22 @@
+#
+# CMakeLists.txt -- code building for common code
+#
+# Copyright (c) 2019 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
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+code_define_module(
+	NAME options
+	SOURCES options.hpp
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cpp/options/options.hpp	Mon Jul 15 13:54:17 2019 +0200
@@ -0,0 +1,156 @@
+/*
+ * options.hpp -- getopt(3) similar interface for C++
+ *
+ * Copyright (c) 2019 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
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef OPTIONS_HPP
+#define OPTIONS_HPP
+
+/**
+ * \file options.hpp
+ * \brief C++ alternative to getopt(3).
+ */
+
+#include <stdexcept>
+#include <string>
+#include <string_view>
+#include <tuple>
+#include <unordered_map>
+#include <vector>
+
+/**
+ * \brief C++ alternative to getopt(3).
+ */
+namespace options {
+
+/**
+ * Store the positional arguments and options.
+ */
+using pack = std::tuple<
+	std::vector<std::string>,
+	std::unordered_multimap<char, std::string>
+>;
+
+/**
+ * Parse a collection of options and arguments.
+ *
+ * This function uses the same format as getopt(3) function, you need specify
+ * each option in the fmt string and add a colon after the option character if
+ * it requires a value.
+ *
+ * If a -- option appears in the argument list, it stops option parsing and all
+ * next tokens are considered arguments even if they start with an hyphen.
+ *
+ * Example of format strings:
+ *
+ * - "abc": are all three boolean options,
+ * - "c:v": v is a boolean option c requires a value.
+ *
+ * Example of invocation:
+ *
+ * - `mycli -v -a`: is similar to `-va` if both 'v' and 'a' are boolean options,
+ * - `mycli -v -- -c`: -c will be a positional argument rather than an option
+ *   but '-v' is still an option.
+ *
+ * \tparam InputIt must dereference a string type (literal, std::string_view or
+ * std::string)
+ * \param it the first item
+ * \param end the next item
+ * \param fmt the format string
+ * \return the result
+ */
+template <typename InputIt>
+inline auto parse(InputIt it, InputIt end, std::string_view fmt) -> pack
+{
+	pack result;
+
+	for (; it != end; ++it) {
+		const std::string_view token(*it);
+
+		if (token == "--") {
+			for (++it; it != end; ++it)
+				std::get<0>(result).push_back(std::string(*it));
+			break;
+		}
+
+		if (token.size() > 0 && token[0] != '-') {
+			std::get<0>(result).push_back(std::string(token));
+			continue;
+		}
+
+		const auto sub = it->substr(1);
+
+		for (std::size_t i = 0U; i < sub.size(); ++i) {
+			const auto idx = fmt.find(sub[i]);
+
+			if (idx == std::string_view::npos)
+				throw std::runtime_error("invalid option");
+
+			if (idx + 1U == fmt.size() || fmt[idx + 1] != ':') {
+				std::get<1>(result).emplace(sub[i], "");
+				continue;
+			}
+
+			if (idx + 1U < sub.size()) {
+				std::get<1>(result).emplace(sub[i], std::string(sub.substr(i + 1)));
+				break;
+			}
+
+			if (++it == end || std::string_view(*it).compare(0U, 1U, "-") == 0)
+				throw std::runtime_error("option require a value");
+
+			std::get<1>(result).emplace(sub[i], std::string(*it));
+		}
+	}
+
+	return result;
+}
+
+/**
+ * Convenient overload with an initializer_list.
+ *
+ * \tparam StringType must be either a std::string or std::string_view
+ * \param args the arguments
+ * \param fmt the format string
+ * \return the result
+ */
+inline auto parse(std::initializer_list<std::string_view> args, std::string_view fmt) -> pack
+{
+	return parse(args.begin(), args.end(), fmt);
+}
+
+/**
+ * Convenient overload for main() arguments.
+ *
+ * \param argc the number of arguments
+ * \param argv the arguments
+ * \param fmt the format string
+ * \return the result
+ */
+inline auto parse(int argc, char** argv, std::string_view fmt) -> pack
+{
+	std::vector<std::string_view> args(argc);
+
+	for (int i = 0; i < argc; ++i)
+		args[i] = argv[i];
+
+	return parse(args.begin(), args.end(), fmt);
+}
+
+
+} // !options
+
+#endif // !OPTIONS_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cpp/options/test/main.cpp	Mon Jul 15 13:54:17 2019 +0200
@@ -0,0 +1,177 @@
+/*
+ * main.cpp -- test options functions
+ *
+ * Copyright (c) 2019 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
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define BOOST_TEST_MODULE "options"
+#include <boost/test/unit_test.hpp>
+
+#include "options.hpp"
+
+BOOST_AUTO_TEST_CASE(boolean)
+{
+	const auto [ args, options ] = options::parse({"-v"}, "v");
+
+	BOOST_TEST(args.size() == 0U);
+	BOOST_TEST(options.size() == 1U);
+	BOOST_TEST(options.count('v'));
+	BOOST_TEST(options.find('v')->second.empty());
+}
+
+BOOST_AUTO_TEST_CASE(boolean_repeat_detached)
+{
+	const auto [ args, options ] = options::parse({"-v", "-v"}, "v");
+
+	BOOST_TEST(args.size() == 0U);
+	BOOST_TEST(options.size() == 2U);
+	BOOST_TEST(options.count('v') == 2U);
+}
+
+BOOST_AUTO_TEST_CASE(boolean_repeat_adjacent)
+{
+	const auto [ args, options ] = options::parse({"-vv"}, "v");
+
+	BOOST_TEST(args.size() == 0U);
+	BOOST_TEST(options.size() == 2U);
+	BOOST_TEST(options.count('v') == 2U);
+}
+
+BOOST_AUTO_TEST_CASE(parameter_detached)
+{
+	const auto [ args, options ] = options::parse({"-c", "config"}, "c:");
+
+	BOOST_TEST(args.size() == 0U);
+	BOOST_TEST(options.size() == 1u);
+	BOOST_TEST(options.count('c'));
+	BOOST_TEST(options.find('c')->second == "config");
+}
+
+BOOST_AUTO_TEST_CASE(parameter_adjacent)
+{
+	const auto [ args, options ] = options::parse({"-cconfig"}, "c:");
+
+	BOOST_TEST(args.size() == 0U);
+	BOOST_TEST(options.size() == 1U);
+	BOOST_TEST(options.count('c'));
+	BOOST_TEST(options.find('c')->second == "config");
+}
+
+BOOST_AUTO_TEST_CASE(mixed_detached)
+{
+	const auto [ args, options ] = options::parse({"-vc", "config"}, "vc:");
+
+	BOOST_TEST(args.size() == 0U);
+	BOOST_TEST(options.size() == 2U);
+	BOOST_TEST(options.count('v'));
+	BOOST_TEST(options.find('v')->second.empty());
+	BOOST_TEST(options.count('c'));
+	BOOST_TEST(options.find('c')->second == "config");
+}
+
+BOOST_AUTO_TEST_CASE(mixed_adjacent)
+{
+	const auto [ args, options ] = options::parse({"-vcconfig"}, "vc:");
+
+	BOOST_TEST(args.size() == 0U);
+	BOOST_TEST(options.size() == 2U);
+	BOOST_TEST(options.count('v'));
+	BOOST_TEST(options.find('v')->second.empty());
+	BOOST_TEST(options.count('c'));
+	BOOST_TEST(options.find('c')->second == "config");
+}
+
+BOOST_AUTO_TEST_CASE(mixed_repeat)
+{
+	const auto [ args, options ] = options::parse({"-vvcconfig"}, "vc:");
+
+	BOOST_TEST(args.size() == 0U);
+	BOOST_TEST(options.size() == 3U);
+	BOOST_TEST(options.count('v') == 2U);
+	BOOST_TEST(options.find('v')->second.empty());
+	BOOST_TEST(options.count('c'));
+	BOOST_TEST(options.find('c')->second == "config");
+}
+
+BOOST_AUTO_TEST_CASE(arguments)
+{
+	const auto [ args, options ] = options::parse({"-c", "config", "install", "-v"}, "vc:");
+
+	BOOST_TEST(args.size() == 1U);
+	BOOST_TEST(args[0] == "install");
+	BOOST_TEST(options.size() == 2U);
+	BOOST_TEST(options.count('v') == 1U);
+	BOOST_TEST(options.find('v')->second.empty());
+	BOOST_TEST(options.count('c'));
+	BOOST_TEST(options.find('c')->second == "config");
+}
+
+BOOST_AUTO_TEST_CASE(stop)
+{
+	const auto [ args, options ] = options::parse({"rm", "-f", "--", "-p"}, "f");
+
+	BOOST_TEST(args.size() == 2U);
+	BOOST_TEST(args[0] == "rm");
+	BOOST_TEST(args[1] == "-p");
+	BOOST_TEST(options.size() == 1U);
+	BOOST_TEST(options.count('f') == 1U);
+	BOOST_TEST(options.find('f')->second.empty());
+}
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(invalid_option)
+{
+	BOOST_REQUIRE_THROW(options::parse({"-x"}, "v"), std::runtime_error);
+}
+
+BOOST_AUTO_TEST_CASE(value_required)
+{
+	BOOST_REQUIRE_THROW(options::parse({"-c", "-v"}, "vc:"), std::runtime_error);
+}
+
+BOOST_AUTO_TEST_CASE(value_required_detached)
+{
+	BOOST_REQUIRE_THROW(options::parse({"-v", "-c"}, "vc:"), std::runtime_error);
+}
+
+BOOST_AUTO_TEST_CASE(value_required_adjacent)
+{
+	BOOST_REQUIRE_THROW(options::parse({"-vc"}, "vc:"), std::runtime_error);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+#if 0
+
+#include <iostream>
+
+#include "options.hpp"
+
+int main()
+{
+	{
+	const auto [ args, options ] = options::parse({"-v"}, "v");
+	}
+	{
+	const auto [ args, options ] = options::parse({"-c", "config"}, "c:");
+	for (const auto& a : args)
+		std::cout << "a => " << a << std::endl;
+	for (const auto& [k,v] : options)
+		std::cout << k << " = " << v << std::endl;
+	}
+
+}
+#endif