aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.cli
diff options
context:
space:
mode:
Diffstat (limited to 'nihil.cli')
-rw-r--r--nihil.cli/CMakeLists.txt2
-rw-r--r--nihil.cli/command.ccm6
-rw-r--r--nihil.cli/nihil.cli.ccm1
-rw-r--r--nihil.cli/parse_flags.ccm523
-rw-r--r--nihil.cli/parse_flags.test.cc523
-rw-r--r--nihil.cli/registry.ccm14
-rw-r--r--nihil.cli/usage_error.ccm2
7 files changed, 1069 insertions, 2 deletions
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 &registry = 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