# HG changeset patch # User David Demelier # Date 1563191657 -7200 # Node ID 11fa64b69530971548c2ba3fce8a347fa029ce7e # Parent 734ce3a26a581b8eafd820321288a06311bc48bb options: reintroduce a very basic getopt(3) alternative diff -r 734ce3a26a58 -r 11fa64b69530 cpp/CMakeLists.txt --- 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) diff -r 734ce3a26a58 -r 11fa64b69530 cpp/options/CMakeLists.txt --- /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 +# +# 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 +) diff -r 734ce3a26a58 -r 11fa64b69530 cpp/options/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 + * + * 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 +#include +#include +#include +#include +#include + +/** + * \brief C++ alternative to getopt(3). + */ +namespace options { + +/** + * Store the positional arguments and options. + */ +using pack = std::tuple< + std::vector, + std::unordered_multimap +>; + +/** + * 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 +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 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 args(argc); + + for (int i = 0; i < argc; ++i) + args[i] = argv[i]; + + return parse(args.begin(), args.end(), fmt); +} + + +} // !options + +#endif // !OPTIONS_HPP diff -r 734ce3a26a58 -r 11fa64b69530 cpp/options/test/main.cpp --- /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 + * + * 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 + +#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 + +#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