changeset 618:1ae8106369e5

Options: initial reimport, closes #705
author David Demelier <markand@malikania.fr>
date Tue, 26 Sep 2017 09:50:02 +0200
parents 266f32919d0a
children da9c2e128cae
files CMakeLists.txt modules/options/CMakeLists.txt modules/options/options.cpp modules/options/options.hpp modules/options/test/main.cpp
diffstat 5 files changed, 638 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Mon Aug 21 11:49:11 2017 +0200
+++ b/CMakeLists.txt	Tue Sep 26 09:50:02 2017 +0200
@@ -52,3 +52,4 @@
 add_subdirectory(modules/executable)
 add_subdirectory(modules/join)
 add_subdirectory(modules/js)
+add_subdirectory(modules/options)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/options/CMakeLists.txt	Tue Sep 26 09:50:02 2017 +0200
@@ -0,0 +1,26 @@
+#
+# CMakeLists.txt -- options module
+#
+# Copyright (c) 2013-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
+# 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.
+#
+
+project(options)
+
+code_define_module(
+    NAME options
+    SOURCES
+        ${options_SOURCE_DIR}/options.cpp
+        ${options_SOURCE_DIR}/options.hpp
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/options/options.cpp	Tue Sep 26 09:50:02 2017 +0200
@@ -0,0 +1,187 @@
+/*
+ * options.cpp -- parse Unix command line options
+ *
+ * Copyright (c) 2015-2017 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.
+ */
+
+#include <cassert>
+
+#include "options.hpp"
+
+namespace option {
+
+namespace {
+
+using iterator = std::vector<std::string>::iterator;
+using args = std::vector<std::string>;
+
+inline bool is_option(const std::string& arg) noexcept
+{
+    return arg.size() >= 2 && arg[0] == '-';
+}
+
+inline bool is_long_option(const std::string& arg) noexcept
+{
+    assert(is_option(arg));
+
+    return arg.size() >= 3 && arg[1] == '-';
+}
+
+inline bool is_short_simple(const std::string& arg) noexcept
+{
+    assert(is_option(arg) && !is_long_option(arg));
+
+    return arg.size() == 2;
+}
+
+void parse_long_option(result& result, args& args, iterator& it, iterator& end, const options& definition)
+{
+    auto arg = *it++;
+    auto opt = definition.find(arg);
+
+    if (opt == definition.end())
+        throw invalid_option(arg);
+
+    // Need argument?
+    if (opt->second) {
+        if (it == end || is_option(*it))
+            throw missing_value(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();
+    }
+}
+
+void parse_short_option_simple(result& result, args& args, iterator& it, iterator &end, const options& definition)
+{
+    /*
+     * Here two cases:
+     *
+     * -v (no option)
+     * -c value
+     */
+    auto arg = *it++;
+    auto opt = definition.find(arg);
+
+    if (opt == definition.end())
+        throw invalid_option(arg);
+
+    // Need argument?
+    if (opt->second) {
+        if (it == end || is_option(*it))
+            throw missing_value(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();
+    }
+}
+
+void parse_short_option_compressed(result& result, args& args, iterator& it, iterator &end, const options& definition)
+{
+    /*
+     * 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;
+
+    for (std::size_t i = 0; i < len; ++i) {
+        auto arg = std::string{'-'} + value[i];
+        auto opt = definition.find(arg);
+
+        if (opt == definition.end())
+            throw invalid_option(arg);
+
+        if (opt->second) {
+            if (i == (len - 1)) {
+                // End of string, get the next argument (see 2.).
+                if (++it == end || is_option(*it))
+                    throw missing_value(arg);
+
+                result.insert(std::make_pair(arg, *it));
+                toremove += 1;
+            } else {
+                result.insert(std::make_pair(arg, value.substr(i + 1)));
+                i = len;
+            }
+        } else
+            result.insert(std::make_pair(arg, ""));
+    }
+
+    it = args.erase(args.begin(), args.begin() + toremove);
+    end = args.end();
+}
+
+void parse_short_option(result& result, args& args, iterator& it, iterator &end, const options& definition)
+{
+    if (is_short_simple(*it))
+        parse_short_option_simple(result, args, it, end, definition);
+    else
+        parse_short_option_compressed(result, args, it, end, definition);
+}
+
+} // !namespace
+
+result read(std::vector<std::string>& args, const options& definition)
+{
+    result result;
+
+    auto it = args.begin();
+    auto end = args.end();
+
+    while (it != end) {
+        if (!is_option(*it))
+            break;
+
+        if (is_long_option(*it))
+            parse_long_option(result, args, it, end, definition);
+        else
+            parse_short_option(result, args, it, end, definition);
+    }
+
+    return result;
+}
+
+result read(int& argc, char**& argv, const options& definition)
+{
+    std::vector<std::string> args;
+
+    for (int i = 0; i < argc; ++i)
+        args.push_back(argv[i]);
+
+    auto before = args.size();
+    auto result = read(args, definition);
+
+    argc -= before - args.size();
+    argv += before - args.size();
+
+    return result;
+}
+
+} // !option
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/options/options.hpp	Tue Sep 26 09:50:02 2017 +0200
@@ -0,0 +1,191 @@
+/*
+ * options.hpp -- parse Unix command line options
+ *
+ * Copyright (c) 2015-2017 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 Basic Unix options parser.
+ */
+
+/**
+ * \page options Options parser.
+ *
+ * ## Export macros
+ *
+ * You must define `OPTIONS_DLL` globally and `OPTIONS_BUILDING_DLL` when
+ * compiling the library if you want a DLL, alternatively you can provide your
+ * own `OPTIONS_EXPORT` macro instead.
+ */
+
+/**
+ * \cond OPTIONS_HIDDEN_SYMBOLS
+ */
+
+#if !defined(OPTIONS_EXPORT)
+#   if defined(OPTIONS_DLL)
+#       if defined(_WIN32)
+#           if defined(OPTIONS_BUILDING_DLL)
+#               define OPTIONS_EXPORT __declspec(dllexport)
+#           else
+#               define OPTIONS_EXPORT __declspec(dllimport)
+#           endif
+#       else
+#           define OPTIONS_EXPORT
+#       endif
+#   else
+#       define OPTIONS_EXPORT
+#   endif
+#endif
+
+/**
+ * \endcond
+ */
+
+#include <exception>
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+/**
+ * Namespace for options parsing.
+ */
+namespace option {
+
+/**
+ * \brief This exception is thrown when an invalid option has been found.
+ */
+class invalid_option : public std::exception {
+private:
+    std::string message_;
+    std::string name_;
+
+public:
+    /**
+     * Construct the exception.
+     *
+     * \param arg the argument missing
+     */
+    inline invalid_option(std::string name)
+        : name_(std::move(name))
+    {
+        message_ = std::string("invalid option: ") + name_;
+    }
+
+    /**
+     * Get the option name.
+     *
+     * \return the name
+     */
+    inline const std::string& name() const noexcept
+    {
+        return name_;
+    }
+
+    /**
+     * Get the error message.
+     *
+     * \return the error message
+     */
+    const char* what() const noexcept override
+    {
+        return message_.c_str();
+    }
+};
+
+/**
+ * \brief This exception is thrown when an option requires a value and no value
+ * has been given.
+ */
+class missing_value : public std::exception {
+private:
+    std::string message_;
+    std::string name_;
+
+public:
+    /**
+     * Construct the exception.
+     *
+     * \param name the option that requires a value
+     */
+    inline missing_value(std::string name)
+        : name_(std::move(name))
+    {
+        message_ = std::string("missing argument for: ") + name_;
+    }
+
+    /**
+     * Get the option name.
+     *
+     * \return the name
+     */
+    inline const std::string& name() const noexcept
+    {
+        return name_;
+    }
+
+    /**
+     * Get the error message.
+     *
+     * \return the error message
+     */
+    const char* what() const noexcept override
+    {
+        return message_.c_str();
+    }
+};
+
+/**
+ * Packed multimap of options.
+ */
+using result = std::multimap<std::string, std::string>;
+
+/**
+ * Define the allowed options.
+ */
+using options = std::map<std::string, bool>;
+
+/**
+ * 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 missing_value
+ * \throw invalid_option
+ */
+OPTIONS_EXPORT result read(std::vector<std::string>& args, const options& definition);
+
+/**
+ * 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 missing_value
+ * \throw invalid_option
+ */
+OPTIONS_EXPORT result read(int& argc, char**& argv, const options& definition);
+
+} // !option
+
+#endif // !OPTIONS_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/options/test/main.cpp	Tue Sep 26 09:50:02 2017 +0200
@@ -0,0 +1,233 @@
+/*
+ * main.cpp -- main test file for OptionParser
+ *
+ * Copyright (c) 2013-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
+ * 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>
+
+/*
+ * Short options.
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(short_options)
+
+BOOST_AUTO_TEST_CASE(simple)
+{
+    std::vector<std::string> args{"-a", "-b"};
+
+    auto pack = option::read(args, {
+        { "-a", false },
+        { "-b", false }
+    });
+
+    BOOST_TEST(pack.size() == 2U);
+    BOOST_TEST(args.size() == 0U);
+    BOOST_TEST(pack.count("-a") != 0);
+    BOOST_TEST(pack.count("-b") != 0);
+}
+
+BOOST_AUTO_TEST_CASE(simple_arg)
+{
+    std::vector<std::string> args{"-v", "-cfoo.conf"};
+
+    auto pack = option::read(args, {
+        { "-v", false },
+        { "-c", true  }
+    });
+
+    BOOST_TEST(pack.size() == 2U);
+    BOOST_TEST(args.size() == 0U);
+    BOOST_TEST(pack.count("-v") != 0);
+    BOOST_TEST(pack.find("-c")->second == "foo.conf");
+}
+
+BOOST_AUTO_TEST_CASE(spaced_arg)
+{
+    std::vector<std::string> args{"-v", "-c", "foo.conf"};
+
+    auto pack = option::read(args, {
+        { "-v", false },
+        { "-c", true  }
+    });
+
+    BOOST_TEST(pack.size() == 2U);
+    BOOST_TEST(args.size() == 0U);
+    BOOST_TEST(pack.count("-v") != 0);
+    BOOST_TEST(pack.find("-c")->second == "foo.conf");
+}
+
+BOOST_AUTO_TEST_CASE(compacted)
+{
+    std::vector<std::string> args{"-abc"};
+
+    auto pack = option::read(args, {
+        { "-a", false },
+        { "-b", false },
+        { "-c", false },
+    });
+
+    BOOST_TEST(pack.size() == 3U);
+    BOOST_TEST(args.size() == 0U);
+    BOOST_TEST(pack.count("-a") != 0);
+    BOOST_TEST(pack.count("-b") != 0);
+    BOOST_TEST(pack.count("-c") != 0);
+}
+
+BOOST_AUTO_TEST_CASE(compacted_arg)
+{
+    std::vector<std::string> args{"-vdcfoo.conf"};
+
+    auto pack = option::read(args, {
+        { "-v", false },
+        { "-d", false },
+        { "-c", true },
+    });
+
+    BOOST_TEST(pack.size() == 3U);
+    BOOST_TEST(args.size() == 0U);
+    BOOST_TEST(pack.count("-v") != 0);
+    BOOST_TEST(pack.count("-d") != 0);
+    BOOST_TEST(pack.find("-c")->second == "foo.conf");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * Long options.
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(long_options)
+
+BOOST_AUTO_TEST_CASE(simple)
+{
+    std::vector<std::string> args{"--fullscreen"};
+
+    auto pack = option::read(args, {
+        { "--verbose",      false },
+        { "--fullscreen",   false }
+    });
+
+    BOOST_TEST(pack.size() == 1U);
+    BOOST_TEST(args.size() == 0U);
+    BOOST_TEST(pack.count("--fullscreen") != 0);
+}
+
+BOOST_AUTO_TEST_CASE(simple_arg)
+{
+    std::vector<std::string> args{"--config", "config.conf", "--level", "2"};
+
+    auto pack = option::read(args, {
+        { "--config",   true },
+        { "--level",    true }
+    });
+
+    BOOST_TEST(pack.size() == 2U);
+    BOOST_TEST(args.size() == 0U);
+    BOOST_TEST(pack.find("--config")->second == "config.conf");
+    BOOST_TEST(pack.find("--level")->second == "2");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * Errors.
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(errors)
+
+BOOST_AUTO_TEST_CASE(stop)
+{
+    std::vector<std::string> args{"-v", "install", "-y", "irccd"};
+    std::vector<std::string> expected{"install", "-y", "irccd"};
+
+    auto pack = option::read(args, {{ "-v", false }});
+
+    BOOST_TEST(pack.size() == 1U);
+    BOOST_TEST(args.size() == 3U);
+    BOOST_TEST(pack.count("-v") != 0);
+    BOOST_TEST(expected == args);
+}
+
+BOOST_AUTO_TEST_CASE(missing_short_arg)
+{
+    std::vector<std::string> args{"-c"};
+
+    BOOST_REQUIRE_THROW(option::read(args, {{ "-c", true }}), option::missing_value);
+}
+
+BOOST_AUTO_TEST_CASE(missing_short_arg2)
+{
+    const option::options options{
+        { "-v", false },
+        { "-c", true }
+    };
+
+    std::vector<std::string> args{"-vc"};
+
+    BOOST_REQUIRE_THROW(option::read(args, options), option::missing_value);
+}
+
+BOOST_AUTO_TEST_CASE(missing_long_arg)
+{
+    const option::options options{
+        { "--config", true }
+    };
+
+    std::vector<std::string> args{"--config"};
+
+    BOOST_REQUIRE_THROW(option::read(args, options), option::missing_value);
+}
+
+BOOST_AUTO_TEST_CASE(invalid_option)
+{
+    const option::options options{
+        { "-v", true }
+    };
+
+    std::vector<std::string> args{"-x"};
+
+    BOOST_REQUIRE_THROW(option::read(args, options), option::invalid_option);
+}
+
+BOOST_AUTO_TEST_CASE(invalid_option2)
+{
+    const option::options options{
+        { "--verbose", true }
+    };
+
+    std::vector<std::string> args{"--destroy"};
+
+    BOOST_REQUIRE_THROW(option::read(args, options), option::invalid_option);
+}
+
+BOOST_AUTO_TEST_CASE(invalid_option3)
+{
+    const option::options options{
+        { "-x", true }
+    };
+
+    std::vector<std::string> args{"-vx"};
+
+    BOOST_REQUIRE_THROW(option::read(args, options), option::invalid_option);
+}
+
+BOOST_AUTO_TEST_SUITE_END()