diff options
| author | Lexi Winter <lexi@le-fay.org> | 2025-07-03 19:46:01 +0100 |
|---|---|---|
| committer | Lexi Winter <lexi@le-fay.org> | 2025-07-03 19:46:01 +0100 |
| commit | e9609dfea5a8210f9e1a23c0d80f53df7664e71a (patch) | |
| tree | c07fe2e0a47ee8d5a93d135752e4d0f2dba7424e | |
| parent | c5a27de4bc520a9ac3538e8960097a8a65428182 (diff) | |
| download | nihil-main.tar.gz nihil-main.tar.bz2 | |
| -rw-r--r-- | .clang-tidy | 1 | ||||
| -rw-r--r-- | nihil.cli/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | nihil.cli/command.ccm | 6 | ||||
| -rw-r--r-- | nihil.cli/nihil.cli.ccm | 1 | ||||
| -rw-r--r-- | nihil.cli/parse_flags.ccm | 523 | ||||
| -rw-r--r-- | nihil.cli/parse_flags.test.cc | 523 | ||||
| -rw-r--r-- | nihil.cli/registry.ccm | 14 | ||||
| -rw-r--r-- | nihil.cli/usage_error.ccm | 2 | ||||
| -rw-r--r-- | nihil.core/errc.ccm | 5 | ||||
| -rw-r--r-- | nihil.std/nihil.std.ccm | 16 |
10 files changed, 1090 insertions, 3 deletions
diff --git a/.clang-tidy b/.clang-tidy index c8d47b0..ded82c1 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -19,6 +19,7 @@ Checks: > -hicpp-named-parameter, -hicpp-vararg, misc-*, + -misc-non-private-member-variables-in-classes, modernize-*, readability-*, -readability-braces-around-statements, diff --git a/nihil.cli/CMakeLists.txt b/nihil.cli/CMakeLists.txt index 8f4d1ea..da711a1 100644 --- a/nihil.cli/CMakeLists.txt +++ b/nihil.cli/CMakeLists.txt @@ -15,6 +15,7 @@ target_sources(nihil.cli command.ccm command_tree.ccm dispatch_command.ccm + parse_flags.ccm registry.ccm usage_error.ccm @@ -29,6 +30,7 @@ if(NIHIL_TESTS) command.test.cc command_tree.test.cc dispatch_command.test.cc + parse_flags.test.cc ) target_link_libraries(nihil.cli.test PRIVATE diff --git a/nihil.cli/command.ccm b/nihil.cli/command.ccm index 1bcb13f..ab7fa1a 100644 --- a/nihil.cli/command.ccm +++ b/nihil.cli/command.ccm @@ -20,6 +20,7 @@ export struct command; // registry.ccm auto register_command(command *cmd) noexcept -> void; +auto unregister_command(command *cmd) noexcept -> void; // A command. If constructed with a handler, this is a "real" command which can be invoked. // Otherwise, it's a stub command that has children in the command tree. @@ -60,7 +61,10 @@ export struct command final command(command &&) = delete; auto operator=(command &&) -> command & = delete; - ~command() = default; + ~command() + { + unregister_command(this); + } // Return the full path for this command. [[nodiscard]] auto path(this command const &self) noexcept -> std::string_view diff --git a/nihil.cli/nihil.cli.ccm b/nihil.cli/nihil.cli.ccm index 5463fd9..9c5cff4 100644 --- a/nihil.cli/nihil.cli.ccm +++ b/nihil.cli/nihil.cli.ccm @@ -4,5 +4,6 @@ export module nihil.cli; export import :command; export import :command_tree; export import :dispatch_command; +export import :parse_flags; export import :registry; export import :usage_error; diff --git a/nihil.cli/parse_flags.ccm b/nihil.cli/parse_flags.ccm new file mode 100644 index 0000000..684aa00 --- /dev/null +++ b/nihil.cli/parse_flags.ccm @@ -0,0 +1,523 @@ +// This source code is released into the public domain. +export module nihil.cli:parse_flags; + +// parse_flags: command-line option processing. + +import nihil.std; +import nihil.core; +import :usage_error; + +namespace nihil::cli { + +// The name of an option: either a short flag, or a long flag, or both, or +// if this is an argument, then neither. +export struct flag_name +{ + // All constructors are implicit to simplify usage of nihil::cli::option(). + + flag_name() = default; + + flag_name(char short_flag) + : m_short_flag(short_flag) + { + } + + flag_name(std::string_view long_flag) + : m_long_flag(long_flag) + { + } + + flag_name(char short_flag, std::string_view long_flag) + : m_short_flag(short_flag) + , m_long_flag(long_flag) + { + } + + [[nodiscard]] auto short_flag(this flag_name const &self) -> std::optional<char> const & + { + return self.m_short_flag; + } + + [[nodiscard]] auto + long_flag(this flag_name const &self) -> std::optional<std::string> const & + { + return self.m_long_flag; + } + + // If an option has neither a short nor long name, then it's an argument. + [[nodiscard]] auto is_argument(this flag_name const &self) -> bool + { + return !self.m_short_flag.has_value() && !self.m_long_flag.has_value(); + } + + // Return this flag as a human-readable string. + [[nodiscard]] auto str(this flag_name const &self) -> std::string + { + auto const &s = self.short_flag(); + auto const &l = self.long_flag(); + if (s && l) + return std::format("-{}|--{}", *s, *l); + else if (s) + return std::format("-{}", *s); + else if (l) + return std::format("--{}", *l); + else + return {}; + } + +private: + std::optional<char> m_short_flag; + std::optional<std::string> m_long_flag; +}; + +// Base class for type-specific flags. +template <typename Flags> +struct option_base +{ + // Create an option that doesn't take an argument. + explicit option_base(flag_name name) + : m_name(name) + { + } + + // Create an option that takes one argument. + explicit option_base(flag_name name, std::string_view argument_name) + : m_name(name) + , m_argument_name(argument_name) + { + } + + option_base(option_base const &) = default; + auto operator=(option_base const &) -> option_base & = default; + option_base(option_base &&) = default; + auto operator=(option_base &&) -> option_base & = default; + + virtual ~option_base() = default; + + [[nodiscard]] auto name(this option_base const &self) -> flag_name const & + { + return self.m_name; + } + + [[nodiscard]] auto + argument_name(this option_base const &self) -> std::optional<std::string> const & + { + return self.m_argument_name; + } + + [[nodiscard]] auto has_argument(this option_base const &self) -> bool + { + return self.m_argument_name.has_value(); + } + + // If parse() is overridden, provide a default parse(arg) that returns an + // unexpected argument error. + [[nodiscard]] virtual auto parse(Flags *, std::string_view) -> std::expected<void, error> + { + return error( + std::format("option -{} does not take an argument", *m_name.short_flag())); + } + + // If parse(arg) is overridden, provide a default parse() that returns a + // missing argument error. + [[nodiscard]] virtual auto parse(Flags *) -> std::expected<void, error> + { + return error(std::format("option -{} requires an argument", *m_name.short_flag())); + } + + [[nodiscard]] virtual auto required() -> bool + { + return true; + } + +private: + flag_name m_name; + std::optional<std::string> m_argument_name; +}; + +// Type-specific flags. + +export template <typename Flags, typename Flag> +struct option; + +// Boolean option. This can only be present (or not) and has no value. +export template <typename Flags> +struct option<Flags, bool> final : option_base<Flags> +{ + option(flag_name name, bool Flags::*ptr) + : option_base<Flags>(name) + , m_ptr(ptr) + { + } + + // Parse a boolean argument. + [[nodiscard]] auto parse(Flags *instance) -> std::expected<void, error> override + { + instance->*m_ptr = true; + return {}; + } + +private: + bool Flags::*m_ptr = nullptr; +}; + +// String option +export template <typename Flags> +struct option<Flags, std::string> final : option_base<Flags> +{ + // Create a string flag. + option(flag_name name, std::string_view argument_name, std::string Flags::*ptr) + : option_base<Flags>(name, argument_name) + , m_ptr(ptr) + { + } + + // Create a string argument.. + option(std::string_view argument_name, std::string Flags::*ptr) + : option_base<Flags>(flag_name(), argument_name) + , m_ptr(ptr) + { + } + + [[nodiscard]] auto + parse(Flags *instance, std::string_view arg) -> std::expected<void, error> override + { + instance->*m_ptr = arg; + return {}; + } + +private: + std::string Flags::*m_ptr = nullptr; +}; + +// Integer option. Don't match char here, because even though that might be useful for +// [u]int8_t, it's confusing when passed an actual char. We exclude both signed and +// unsigned char to avoid different behaviour based on whether char is signed. +export template <typename Flags, std::integral Integer> +requires(!std::same_as<std::make_signed<Integer>, char>) +struct option<Flags, Integer> final : option_base<Flags> +{ + // Create a string flag. + option(flag_name name, std::string_view argument_name, Integer Flags::*ptr) + : option_base<Flags>(name, argument_name) + , m_ptr(ptr) + { + } + + // Create a string argument.. + option(std::string_view argument_name, Integer Flags::*ptr) + : option_base<Flags>(flag_name(), argument_name) + , m_ptr(ptr) + { + } + + [[nodiscard]] auto + parse(Flags *instance, std::string_view arg) -> std::expected<void, error> override + { + // If we wanted to be locale-independent, we could use std::from_chars here. + // However, users probably expect numbers to be parsed using their locale. + + skipws(&arg); + if (arg.empty()) + return error("expected an integer"); + + if (std::is_unsigned_v<Integer> && arg[0] == '-') + return error("expected a non-negative integer"); + + auto strm = std::istringstream(arg); + auto i = std::conditional_t<std::is_signed_v<Integer>, std::intmax_t, + std::uintmax_t>{}; + + if (!(strm >> i)) + return error("expected an integer"); + + if (std::cmp_greater(i, std::numeric_limits<Integer>::max())) + return error("value too large"); + if (std::cmp_less(i, std::numeric_limits<Integer>::min())) + return error("value too small"); + + instance->*m_ptr = i; + return {}; + } + +private: + Integer Flags::*m_ptr = nullptr; +}; + +// Optional argument +export template <typename Flags, typename Base> +struct option<Flags, std::optional<Base>> final : option_base<Flags> +{ + option(flag_name name, std::string_view argument_name, std::optional<Base> Flags::*ptr) + : option_base<Flags>(name, argument_name) + , m_ptr(ptr) + , m_base(name, argument_name, &proxy::m_base_value) + { + } + + option(std::string_view argument_name, std::optional<Base> Flags::*ptr) + : option_base<Flags>(flag_name(), argument_name) + , m_ptr(ptr) + , m_base(argument_name, &proxy::m_base_value) + { + } + + [[nodiscard]] auto required() -> bool override + { + return false; + } + + [[nodiscard]] auto + parse(Flags *instance, std::string_view arg) -> std::expected<void, error> override + { + auto p = proxy{}; + auto ret = m_base.parse(&p, arg); + if (ret) + instance->*m_ptr = p.m_base_value; + return ret; + } + +private: + struct proxy + { + Base m_base_value; + }; + std::optional<Base> Flags::*m_ptr; + option<proxy, Base> m_base; +}; + +export template <typename Flags, typename T> +option(char, T Flags::*) -> option<Flags, T>; + +export template <typename Flags, typename T> +option(T Flags::*) -> option<Flags, T>; + +export template <typename Flags, typename Argname, typename T> +option(char, Argname, T Flags::*) -> option<Flags, T>; + +export template <typename Flags, typename Argname, typename T> +option(Argname, T Flags::*) -> option<Flags, T>; + +// Flags parser. This is constructed from a list of flags. +export template <typename Flags> +struct options final +{ + // Create a new flag set from a list of flags. + explicit options(auto &&...args) + { + (this->add_flag(args), ...); + } + + // Fetch an option by its letter. + [[nodiscard]] auto option(this options const &self, char c) + -> std::optional<std::shared_ptr<option_base<Flags>>> + { + auto it = self.m_short_flags.find(c); + if (it != self.m_short_flags.end()) + return it->second; + return {}; + } + + // Fetch all options. + [[nodiscard]] auto all_options(this options const &self) + { + return self.m_all_flags | std::views::all; + } + + // Fetch all arguments. + [[nodiscard]] auto arguments(this options const &self) + { + return self.m_arguments | std::views::all; + } + +private: + // All flags we understand, used for generating usage. + std::vector<std::shared_ptr<option_base<Flags>>> m_all_flags; + // Flags which have a short option. + std::map<char, std::shared_ptr<option_base<Flags>>> m_short_flags; + // Flags which have a long option. + std::map<std::string, std::shared_ptr<option_base<Flags>>> m_long_flags; + // Options which are arguments rather than flags. + std::vector<std::shared_ptr<option_base<Flags>>> m_arguments; + + template <typename T> + auto add_flag(this options &self, cli::option<Flags, T> flag_) -> void + { + auto fptr = std::make_shared<cli::option<Flags, T>>(std::move(flag_)); + auto &name = fptr->name(); + + if (name.is_argument()) { + self.m_arguments.emplace_back(std::move(fptr)); + return; + } + + self.m_all_flags.emplace_back(fptr); + + if (auto short_flag = name.short_flag(); short_flag.has_value()) + self.m_short_flags.emplace(std::pair{*short_flag, fptr}); + + if (auto long_flag = name.long_flag(); long_flag.has_value()) + self.m_long_flags.emplace(std::pair{*long_flag, fptr}); + } +}; + +// Return a POSIX usage statement for the provided options. +export template <typename Flags> +[[nodiscard]] auto posix_usage(options<Flags> const &opts) -> std::string +{ + auto bits = std::deque<std::string>(); + // bits[0] is for boolean short flags. + bits.emplace_back(); + + for (auto &&opt : opts.all_options()) { + auto &arg_name = opt->argument_name(); + auto name = opt->name(); + auto s = name.short_flag(); + auto l = name.long_flag(); + if (!s && !l) + continue; + + if (!arg_name.has_value()) { + if (s && !l) + bits[0] += *s; + else if (s && l) + bits.emplace_back(std::format("[-{}|--{}]", *s, *l)); + else if (l) + bits.emplace_back(std::format("[--{}]", *l)); + } else { + if (opt->required()) + bits.emplace_back(std::format("{} <{}>", name.str(), *arg_name)); + else + bits.emplace_back(std::format("[{} <{}>]", name.str(), *arg_name)); + } + } + + // Format boolean options properly, or remove them if there aren't any. + if (bits[0].empty()) + bits.erase(bits.begin()); + else + bits[0] = std::format("[-{}]", bits[0]); + + // No join_with in LLVM yet. + auto ret = std::string(); + for (auto &&bit : bits) { + if (!ret.empty()) + ret += " "; + ret += bit; + } + return ret; +} + +// Parse the provided argument vector in the POSIX style. +export template <typename Flags> +[[nodiscard]] auto posix_parse(options<Flags> const &self, std::ranges::range auto &&argv) + -> std::expected<Flags, error> +{ + auto ret = Flags(); + + auto first = std::ranges::begin(argv); + auto last = std::ranges::end(argv); + + while (first != last) { + auto arg1 = std::string_view(*first); + + // A flag should start with a '-'; if not, we're done. + if (arg1.empty()) + break; + if (arg1[0] != '-') + break; + // A '-' by itself is an argument, not a flag. + if (arg1.size() == 1) + break; + // The special flag '--' terminates parsing. + if (arg1[1] == '-') { + ++first; + break; + } + + // Now we have a '-' followed by one or more flags. + arg1 = arg1.substr(1); + + while (!arg1.empty()) { + auto opt = self.option(arg1[0]); + if (!opt) + return error(std::format("-{}: unknown option", arg1[0])); + + // Eat this option. + arg1 = arg1.substr(1); + + // If this option doesn't take an argument, parse it and continue. + if (!(*opt)->has_argument()) { + if (auto perr = (*opt)->parse(&ret); !perr) + return error(std::format("{}: {}", (*opt)->name().str(), + perr.error())); + continue; + } + + // Otherwise, extract the argument, which could be the rest of this + // string (if there is any), or the next string. + auto arg = arg1; + if (arg.empty()) { + ++first; + if (first == last) + return error( + std::format("{}: argument required", + (*opt)->name().str())); + arg = std::string_view(*first); + } + + if (auto perr = (*opt)->parse(&ret, arg); !perr) + return error( + std::format("{}: {}", (*opt)->name().str(), perr.error())); + + // Move to the next string in the vector. + break; + } + + ++first; + } + + // Everything remaining should be arguments. + auto args = self.arguments(); + auto argument_it = std::ranges::begin(args); + + while (first != last) { + // We ran out of arguments but still have args left. + if (argument_it == args.end()) + return error("too many arguments"); + + if (auto aerr = (*argument_it)->parse(&ret, *first); !aerr) + return std::unexpected(aerr.error()); + + ++argument_it; + ++first; + } + + // We ran out of args but still have arguments left. See if they're required. + // This doesn't handle optional arguments followed by required arguments, but + // it's not clear if that's useful. + if (argument_it != std::ranges::end(args) && (*argument_it)->required()) + return error("not enough arguments"); + + return ret; +} + +// Parse the provided (argc, argv) pair in the POSIX style. This follows the behaviour +// of getopt(), i.e. argv[0] is assumed to be the program name. However, unlike getopt +// the input parameters are not modified. +export template <typename Flags> +[[nodiscard]] auto posix_parse(options<Flags> const &flags, int argc, char *const argv[]) + -> std::expected<Flags, error> +{ + // It's unusual, but possible, for main() to be invoked with no arguments + // at all. Handle that case by providing an empty vector; otherwise, skip + // the first argument. + if (argc > 0) { + --argc; + ++argv; + } + + return posix_parse(flags, std::span(argv, argc)); +} + +} // namespace nihil::cli diff --git a/nihil.cli/parse_flags.test.cc b/nihil.cli/parse_flags.test.cc new file mode 100644 index 0000000..5fa84a2 --- /dev/null +++ b/nihil.cli/parse_flags.test.cc @@ -0,0 +1,523 @@ +// This source code is released into the public domain. + +#include <catch2/catch_test_macros.hpp> + +import nihil.std; +import nihil.cli; +import nihil.core; + +namespace { + +inline auto constexpr *test_tags = "[nihil][nihil.cli][nihil.cli_parse_flags]"; + +SCENARIO("Parsing several flags with flag_parser", test_tags) +{ + using namespace std::literals; + using nihil::cli::option; + using nihil::cli::options; + + GIVEN ("A flag definition") { + struct test_flags + { + bool a_flag{}; + bool b_flag{}; + std::string s_flag; + }; + + auto flags = options<test_flags>(option('a', &test_flags::a_flag), + option('b', &test_flags::b_flag), + option('s', "sflag", &test_flags::s_flag)); + + WHEN ("We parse '-a'") { + auto ret = posix_parse(flags, std::array{"-a"sv}).value(); + + THEN ("a_flag is set") { + REQUIRE(ret.a_flag); + } + + AND_THEN ("The other flags are not") { + REQUIRE(!ret.b_flag); + REQUIRE(ret.s_flag.empty()); + } + } + + WHEN ("We parse '-ab'") { + auto ret = posix_parse(flags, std::array{"-ab"sv}).value(); + + THEN ("a_flag and b_flag are set") { + REQUIRE(ret.a_flag); + REQUIRE(ret.b_flag); + } + + AND_THEN ("s_flag is not set") { + REQUIRE(ret.s_flag.empty()); + } + } + + WHEN ("We parse '-sarg'") { + auto ret = posix_parse(flags, std::array{"-sarg"sv}).value(); + + THEN ("s_flag is set to 'arg'") { + REQUIRE(ret.s_flag == "arg"); + } + + AND_THEN ("The other flags are not set") { + REQUIRE(!ret.a_flag); + REQUIRE(!ret.b_flag); + } + } + + WHEN ("We parse '-s arg'") { + auto ret = posix_parse(flags, std::array{"-s"sv, "arg"sv}).value(); + + THEN ("s_flag is set to 'arg'") { + REQUIRE(ret.s_flag == "arg"); + } + + AND_THEN ("The other flags are not set") { + REQUIRE(!ret.a_flag); + REQUIRE(!ret.b_flag); + } + } + + WHEN ("We parse '-a -sarg'") { + auto ret = posix_parse(flags, std::array{"-a"sv, "-sarg"sv}).value(); + + THEN ("a_flag is set and s_flag is set to 'arg'") { + REQUIRE(ret.a_flag); + REQUIRE(ret.s_flag == "arg"); + } + + AND_THEN ("b_flag is not set") { + REQUIRE(!ret.b_flag); + } + } + + WHEN ("We parse '-asarg'") { + auto ret = posix_parse(flags, std::array{"-asarg"sv}).value(); + + THEN ("a_flag is set and s_flag is set to 'arg'") { + REQUIRE(ret.a_flag); + REQUIRE(ret.s_flag == "arg"); + } + + AND_THEN ("b_flag is not set") { + REQUIRE(!ret.b_flag); + } + } + } +} + +SCENARIO("Calling flag_parser with incorrect options", test_tags) +{ + using namespace std::literals; + using nihil::cli::option; + using nihil::cli::options; + + GIVEN ("A flag definition") { + struct test_flags + { + bool a_flag{}; + std::string s_flag; + }; + + auto flags = options<test_flags>(option('a', &test_flags::a_flag), + option('s', "sflag", &test_flags::s_flag)); + + WHEN ("We parse '-x'") { + auto ret = posix_parse(flags, std::array{"-x"sv}); + + THEN ("An unknown argument error is returned") { + REQUIRE(!ret); + REQUIRE(std::format("{}", ret.error()) == "-x: unknown option"); + } + } + + WHEN ("We parse '-s'") { + auto ret = posix_parse(flags, std::array{"-s"sv}); + + THEN ("A missing argument error is returned") { + REQUIRE(!ret); + REQUIRE(std::format("{}", ret.error()) == "-s: argument required"); + } + } + } +} + +SCENARIO("Terminating the option list with '--'", test_tags) +{ + using namespace std::literals; + using nihil::cli::option; + using nihil::cli::options; + + GIVEN ("A flag parser with -a and -b options") { + struct test_flags + { + bool a_flag{}; + bool b_flag{}; + std::string arg1; + }; + + auto flags = options<test_flags>(option('a', &test_flags::a_flag), + option('b', &test_flags::b_flag), + option("arg1", &test_flags::arg1)); + + WHEN ("We parse '-a -- -b'") { + auto ret = posix_parse(flags, std::array{"-a"sv, "--"sv, "-b"sv}); + + THEN ("The parse was successful") { + if (!ret) { + INFO(ret.error()); + FAIL(); + } + } + AND_THEN ("a_flag is set and b_flag is not") { + REQUIRE(ret->a_flag == true); + REQUIRE(ret->b_flag == false); + } + AND_THEN ("arg1 is set") { + REQUIRE(ret->arg1 == "-b"); + } + } + } +} + +SCENARIO("Creating a posix_usage() string", test_tags) +{ + using namespace std::literals; + using nihil::cli::option; + using nihil::cli::options; + + GIVEN ("A flag parser with only boolean options") { + struct test_flags + { + bool a_flag{}; + bool b_flag{}; + }; + + auto flags = options<test_flags>(option('a', &test_flags::a_flag), + option('b', &test_flags::b_flag)); + + WHEN ("We call posix_usage()") { + auto usage = posix_usage(flags); + + THEN ("The option list is '[-ab]'") { + REQUIRE(usage == "[-ab]"); + } + } + } + + GIVEN ("A flag parser with boolean and string options") { + struct test_flags + { + bool a_flag{}; + bool b_flag{}; + std::string c_flag; + std::optional<std::string> d_flag; + }; + + auto flags = options<test_flags>(option('a', &test_flags::a_flag), + option('b', &test_flags::b_flag), + option('c', "cflag", &test_flags::c_flag), + option('d', "dflag", &test_flags::d_flag)); + + WHEN ("We call posix_usage()") { + auto usage = posix_usage(flags); + + THEN ("The option list is '[-ab] -c <cflag> [-d <dflag>]'") { + REQUIRE(usage == "[-ab] -c <cflag> [-d <dflag>]"); + } + } + } + + GIVEN ("A flag parser with only string options") { + struct test_flags + { + std::string c_flag; + std::string d_flag; + }; + + auto flags = options<test_flags>(option('c', "cflag", &test_flags::c_flag), + option('d', "dflag", &test_flags::d_flag)); + + WHEN ("We call posix_usage()") { + auto usage = posix_usage(flags); + + THEN ("The option list is '-c <cflag> -d <dflag>'") { + REQUIRE(usage == "-c <cflag> -d <dflag>"); + } + } + } +} + +SCENARIO("Calling posix_parse() with an (argc, argv) vector", test_tags) +{ + using namespace std::literals; + using nihil::cli::option; + using nihil::cli::options; + + GIVEN ("A flag definition") + { + struct test_flags + { + bool a_flag{}; + bool b_flag{}; + std::string s_flag; + }; + + auto flags = options<test_flags>(option('a', &test_flags::a_flag), + option('b', &test_flags::b_flag), + option('s', "sflag", &test_flags::s_flag)); + + WHEN ("We parse '-a -s foo'") { + // const_cast the args to char** to match the actual signature of main(). + auto argv = std::array{"progname", "-a", "-s", "foo", + static_cast<char const *>(nullptr)}; + auto ret = posix_parse(flags, argv.size() - 1, + const_cast<char **>(argv.data())) + .value(); + + THEN ("a_flag and s_flag are set") { + REQUIRE(ret.a_flag); + REQUIRE(ret.s_flag == "foo"); + } + + AND_THEN ("b_flag is not set") { + REQUIRE(!ret.b_flag); + } + } + } +} + +SCENARIO("Calling posix_parse() with required arguments", test_tags) +{ + using namespace std::literals; + using nihil::cli::option; + using nihil::cli::options; + + GIVEN ("An options parser with two required arguments") + { + struct test_flags + { + std::string arg1; + std::string arg2; + }; + + auto flags = options<test_flags>(option("arg1", &test_flags::arg1), + option("arg2", &test_flags::arg2)); + + WHEN ("We parse 'foo bar'") { + auto ret = posix_parse(flags, std::array{"foo", "bar"}); + + THEN ("The parse was successful") { + if (!ret) { + INFO(ret.error()); + FAIL(); + } + } + AND_THEN("Both arg1 and arg2 are set correctly") + { + REQUIRE(ret->arg1 == "foo"); + REQUIRE(ret->arg2 == "bar"); + } + } + + WHEN ("We parse '-- -foo -bar'") { + auto ret = posix_parse(flags, std::array{"--", "-foo", "-bar"}); + + THEN ("The parse was successful") { + if (!ret) { + INFO(ret.error()); + FAIL(); + } + } + AND_THEN("Both arg1 and arg2 are set correctly") + { + REQUIRE(ret->arg1 == "-foo"); + REQUIRE(ret->arg2 == "-bar"); + } + } + } +} + +SCENARIO("Calling posix_parse() with required and optional arguments", test_tags) +{ + using namespace std::literals; + using nihil::cli::option; + using nihil::cli::options; + + GIVEN ("An options parser with a required and an optional argument") + { + struct test_flags + { + std::string arg1; + std::optional<std::string> arg2; + }; + + auto flags = options<test_flags>(option("arg1", &test_flags::arg1), + option("arg2", &test_flags::arg2)); + + WHEN ("We parse 'foo bar'") { + auto ret = posix_parse(flags, std::array{"foo", "bar"}); + + THEN ("The parse was successful") { + if (!ret) { + INFO(ret.error()); + FAIL(); + } + } + AND_THEN("Both arg1 and arg2 are set correctly") + { + REQUIRE(ret->arg1 == "foo"); + REQUIRE(ret->arg2 == "bar"); + } + } + + WHEN ("We parse 'foo'") { + auto ret = posix_parse(flags, std::array{"foo"}); + + THEN ("The parse was successful") { + if (!ret) { + INFO(ret.error()); + FAIL(); + } + } + AND_THEN("arg1 is set and arg2 is not") + { + REQUIRE(ret->arg1 == "foo"); + REQUIRE(ret->arg2.has_value() == false); + } + } + } +} + +SCENARIO("Parsing integers with posix_parse()", test_tags) +{ + using namespace std::literals; + using nihil::cli::option; + using nihil::cli::options; + + GIVEN ("An options parser with an int16_t flag") + { + struct test_flags + { + std::int16_t iflag{}; + }; + + auto flags = options<test_flags>(option('i', "iflag", &test_flags::iflag)); + + WHEN ("The value is a small positive integer") { + auto ret = posix_parse(flags, std::array{"-i", "30000"}); + + THEN ("The parse was successful") { + if (!ret) { + INFO(ret.error()); + FAIL(); + } + } + AND_THEN("iflag is set correctly") + { + REQUIRE(ret->iflag == 30000); + } + } + + WHEN ("The value is a small negative integer") { + auto ret = posix_parse(flags, std::array{"-i", "-30000"}); + + THEN ("The parse was successful") { + if (!ret) { + INFO(ret.error()); + FAIL(); + } + } + AND_THEN("iflag is set correctly") + { + REQUIRE(ret->iflag == -30000); + } + } + + WHEN ("The value is a large positive integer") { + auto ret = posix_parse(flags, std::array{"-i", "40000"}); + + THEN ("The parse failed") { + REQUIRE(!ret); + REQUIRE(std::format("{}", ret.error()) == "-i: value too large"); + } + } + + WHEN ("The value is a large negative integer") { + auto ret = posix_parse(flags, std::array{"-i", "-40000"}); + + THEN ("The parse failed") { + REQUIRE(!ret); + REQUIRE(std::format("{}", ret.error()) == "-i: value too small"); + } + } + + WHEN ("The value is not a number") { + auto ret = posix_parse(flags, std::array{"-i", "foo"}); + + THEN ("The parse failed") { + REQUIRE(!ret); + REQUIRE(std::format("{}", ret.error()) == + "-i: expected an integer"); + } + } + } + + GIVEN ("An options parser with a uint16_t flag") + { + struct test_flags + { + std::uint16_t iflag{}; + }; + + auto flags = options<test_flags>(option('i', "iflag", &test_flags::iflag)); + + WHEN ("The value is a positive integer") { + auto ret = posix_parse(flags, std::array{"-i", "60000"}); + + THEN ("The parse was successful") { + if (!ret) { + INFO(ret.error()); + FAIL(); + } + } + AND_THEN("iflag is set correctly") + { + REQUIRE(ret->iflag == 60000); + } + } + + WHEN ("The value is a large positive integer") { + auto ret = posix_parse(flags, std::array{"-i", "80000"}); + + THEN ("The parse failed") { + REQUIRE(!ret); + REQUIRE(std::format("{}", ret.error()) == "-i: value too large"); + } + } + + WHEN ("The value is a negative integer") { + auto ret = posix_parse(flags, std::array{"-i", "-1"}); + + THEN ("The parse failed") { + REQUIRE(!ret); + REQUIRE(std::format("{}", ret.error()) == + "-i: expected a non-negative integer"); + } + } + + WHEN ("The value is not a number") { + auto ret = posix_parse(flags, std::array{"-i", "foo"}); + + THEN ("The parse failed") { + REQUIRE(!ret); + REQUIRE(std::format("{}", ret.error()) == + "-i: expected an integer"); + } + } + } +} + +} // anonymous namespace diff --git a/nihil.cli/registry.ccm b/nihil.cli/registry.ccm index 5e31195..1b104b0 100644 --- a/nihil.cli/registry.ccm +++ b/nihil.cli/registry.ccm @@ -31,4 +31,18 @@ auto register_command(command *cmd) noexcept -> void get_registry().emplace_back(cmd, null_deleter); } +// Unregister a command. This is not very efficient, but it shouldn't usually be called +// except during testing. +auto unregister_command(command *cmd) noexcept -> void +{ + auto ®istry = get_registry(); + + auto it = std::ranges::find_if(registry, [=] (auto c) -> bool { + return c.get() == cmd; + }); + + if (it != std::ranges::end(registry)) + registry.erase(it); +} + } // namespace nihil diff --git a/nihil.cli/usage_error.ccm b/nihil.cli/usage_error.ccm index 497af89..5d52573 100644 --- a/nihil.cli/usage_error.ccm +++ b/nihil.cli/usage_error.ccm @@ -8,7 +8,7 @@ namespace nihil { // Exception thrown to indicate invalid command-line arguments. export struct usage_error : error { - explicit usage_error(std::string_view what) : error(what) {} + explicit usage_error(std::string_view const what) : error(what) {} }; } // namespace nihil diff --git a/nihil.core/errc.ccm b/nihil.core/errc.ccm index fe36274..7f57fec 100644 --- a/nihil.core/errc.ccm +++ b/nihil.core/errc.ccm @@ -9,9 +9,10 @@ export enum struct errc : std::uint8_t { no_error = 0, /////////////////////////////////////////////////////////////// - // nihil.command + // nihil.cli incomplete_command, + usage_error, /////////////////////////////////////////////////////////////// // nihil.ucl @@ -43,6 +44,8 @@ struct nihil_error_category final : std::error_category return "No error"; case errc::incomplete_command: return "Incomplete command"; + case errc::usage_error: + return "Usage error"; case errc::empty_string: return "Empty string is not permitted"; case errc::invalid_unit: diff --git a/nihil.std/nihil.std.ccm b/nihil.std/nihil.std.ccm index cb6a46d..1bae1f4 100644 --- a/nihil.std/nihil.std.ccm +++ b/nihil.std/nihil.std.ccm @@ -18,6 +18,7 @@ module; #include <compare> #include <concepts> #include <coroutine> +#include <deque> #include <exception> #include <expected> #include <filesystem> @@ -158,10 +159,12 @@ using std::int8_t; using std::int16_t; using std::int32_t; using std::int64_t; +using std::intmax_t; using std::uint8_t; using std::uint16_t; using std::uint32_t; using std::uint64_t; +using std::uintmax_t; // <cstdlib> using std::exit; @@ -176,6 +179,9 @@ using std::rename; // <cstring> using std::strerror; +// <deque> +using std::deque; + // <exception> using std::current_exception; using std::exception; @@ -248,6 +254,7 @@ using std::ostream_iterator; using std::initializer_list; // <iterator> +using std::advance; using std::back_insert_iterator; using std::back_inserter; using std::input_iterator; @@ -361,6 +368,7 @@ using std::ranges::operator|; using std::ranges::to; namespace views { +using std::ranges::views::all; using std::ranges::views::drop; using std::ranges::views::split; using std::ranges::views::take_while; @@ -420,7 +428,9 @@ using std::make_error_condition; using std::system_error; // <type_traits> +using std::add_pointer; using std::add_pointer_t; +using std::conditional; using std::conditional_t; using std::false_type; using std::invoke_result; @@ -450,6 +460,12 @@ using std::is_reference; using std::is_reference_v; using std::is_same; using std::is_same_v; +using std::is_signed; +using std::is_signed_v; +using std::is_unsigned; +using std::is_unsigned_v; +using std::make_signed; +using std::make_unsigned; using std::remove_const_t; using std::remove_cv_t; using std::remove_cvref_t; |
