From 378dd663a402fe196f2b56c6413eb3f623aecbbf Mon Sep 17 00:00:00 2001 From: Lexi Winter Date: Tue, 1 Jul 2025 19:18:42 +0100 Subject: cli: refactoring --- nihil.cli/CMakeLists.txt | 11 +- nihil.cli/command.cc | 44 -------- nihil.cli/command.ccm | 101 +++++++++++++----- nihil.cli/command.test.cc | 57 +++++++++++ nihil.cli/command_node.cc | 40 -------- nihil.cli/command_node.ccm | 26 ----- nihil.cli/command_tree.cc | 169 ------------------------------ nihil.cli/command_tree.ccm | 232 ++++++++++++++++++++++++++++-------------- nihil.cli/dispatch_command.cc | 42 ++++---- nihil.cli/nihil.cli.ccm | 8 +- nihil.cli/registry.cc | 22 ++-- nihil.cli/registry.ccm | 3 +- nihil.cli/test.cc | 6 +- 13 files changed, 331 insertions(+), 430 deletions(-) delete mode 100644 nihil.cli/command.cc create mode 100644 nihil.cli/command.test.cc delete mode 100644 nihil.cli/command_node.cc delete mode 100644 nihil.cli/command_node.ccm delete mode 100644 nihil.cli/command_tree.cc (limited to 'nihil.cli') diff --git a/nihil.cli/CMakeLists.txt b/nihil.cli/CMakeLists.txt index 9f20cf7..41fae52 100644 --- a/nihil.cli/CMakeLists.txt +++ b/nihil.cli/CMakeLists.txt @@ -1,22 +1,22 @@ # This source code is released into the public domain. add_library(nihil.cli STATIC) -target_link_libraries(nihil.cli PRIVATE nihil.std nihil.util) +target_link_libraries(nihil.cli PRIVATE + nihil.std + nihil.generator + nihil.util +) target_sources(nihil.cli PUBLIC FILE_SET modules TYPE CXX_MODULES FILES nihil.cli.ccm command.ccm command_tree.ccm - command_node.ccm dispatch_command.ccm registry.ccm usage_error.ccm PRIVATE - command.cc - command_tree.cc - command_node.cc dispatch_command.cc registry.cc ) @@ -26,6 +26,7 @@ if(NIHIL_TESTS) add_executable(nihil.cli.test test.cc + command.test.cc ) target_link_libraries(nihil.cli.test PRIVATE nihil.cli diff --git a/nihil.cli/command.cc b/nihil.cli/command.cc deleted file mode 100644 index 6271cc6..0000000 --- a/nihil.cli/command.cc +++ /dev/null @@ -1,44 +0,0 @@ -// This source code is released into the public domain. -module; - -// For EX_USAGE. While is deprecated, there's no other standard -// exit code for 'usage error'; some programs use 2 (common on Linux), but -// 2 is also used for many other exit codes. -#include - -module nihil.cli; - -import nihil.std; -import nihil.error; -import :registry; - -namespace nihil { - -//NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -command::command(std::string_view path, std::string_view usage, - command_function_t handler) - : command_node(path) - , m_usage(usage) - , m_handler(std::move(handler)) -{ - register_command(this); -} - -auto command::usage(this command const &self) noexcept -> std::string_view -{ - return self.m_usage; -} - -auto command::invoke(int argc, char **argv) const - -> std::expected -{ - try { - return std::invoke(m_handler, argc, argv); - } catch (usage_error const &err) { - std::print(std::cerr, "{}\n", err.what()); - std::print(std::cerr, "usage: {} {}", path(), usage()); - return EX_USAGE; - } -} - -} // namespace nihil diff --git a/nihil.cli/command.ccm b/nihil.cli/command.ccm index da3444a..dc3f29a 100644 --- a/nihil.cli/command.ccm +++ b/nihil.cli/command.ccm @@ -1,44 +1,97 @@ -/* - * This source code is released into the public domain. - */ - +// This source code is released into the public domain. module; +// For EX_USAGE. While is deprecated, there's no other standard +// exit code for 'usage error'; some programs use 2 (common on Linux), but +// 2 is also used for many other exit codes. +#include + export module nihil.cli:command; -import nihil.error; +// command_node represents a possibly-invocable command. + import nihil.std; -import :command_node; +import nihil.core; +import nihil.error; +import :usage_error; namespace nihil { export struct command; -/* - * A command that can be invoked. Instantiating a command adds this command - * to the global command table. If an error occurs, the program will abort. - */ +// registry.ccm +auto register_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. +// +// Because commands are intended to be global objects, they are not copyable or movable. +// Creating an object automatically registers it in the list of commands. +export struct command final +{ + using command_handler_t = std::function; + + command(std::string_view const path, std::string_view const usage, command_handler_t handler) noexcept + : m_path(path) + , m_usage(usage) + , m_handler(std::move(handler)) + { + register_command(this); + } + + explicit command(std::string_view const path) noexcept + : m_path(path) + { + } + + // Not copyable. + command(command const &) = delete; + auto operator=(command const &) -> command & = delete; + + // Not movable. + command(command &&) = delete; + auto operator=(command &&) -> command & = delete; + + ~command() = default; -using command_handler_t = int (int, char **); -using command_function_t = std::function; + // Return the full path for this command. + [[nodiscard]] auto path(this command const &self) noexcept -> std::string_view + { + return self.m_path; + } -export struct command final : command_node { - command(std::string_view path, std::string_view usage, - command_function_t); + // Return the one-line usage summary for this command. + [[nodiscard]] auto usage(this command const &self) noexcept -> std::string_view + { + return self.m_usage; + } - command(std::string_view path, std::string_view usage, auto &&fn) - : command(path, usage, command_function_t(fn)) - {} + // Test if this command can be invoked. + [[nodiscard]] auto invocable(this command const &self) noexcept -> bool + { + return static_cast(self.m_handler); + } - [[nodiscard]] auto usage(this command const &) noexcept - -> std::string_view; + // Invoke this command and return its result. + [[nodiscard]] auto invoke(this command const &self, int argc, char **argv) + -> std::expected + { + if (!self.m_handler) + return error(errc::incomplete_command); - [[nodiscard]] auto invoke(int argc, char **argv) const - -> std::expected override; + try { + return std::invoke(self.m_handler, argc, argv); + } catch (usage_error const &err) { + std::println(std::cerr, "{}", err.what()); + std::println(std::cerr, "usage: {} {}", self.path(), self.usage()); + return EX_USAGE; + } + } private: - std::string_view m_usage; - command_function_t m_handler; + std::string m_path; + std::string m_usage; + command_handler_t m_handler; }; } // namespace nihil diff --git a/nihil.cli/command.test.cc b/nihil.cli/command.test.cc new file mode 100644 index 0000000..5ac52ed --- /dev/null +++ b/nihil.cli/command.test.cc @@ -0,0 +1,57 @@ +// This source code is released into the public domain. + +#include + +import nihil.std; +import nihil.cli; + +namespace { +inline constexpr auto *test_tags = "[nihil][nihil.cli][nihil.cli.command]"; + +TEST_CASE("nihil::command invariants", test_tags) +{ + static_assert(!std::move_constructible); + static_assert(!std::copy_constructible); + static_assert(!std::is_copy_assignable_v); + static_assert(!std::is_move_assignable_v); + + static_assert(std::destructible); +} + +SCENARIO("A command has a path", test_tags) +{ + GIVEN ("A command object with a path") { + auto cmd = nihil::command("foo bar baz"); + + THEN ("The path is correct") { + REQUIRE(cmd.path() == "foo bar baz"); + } + } +} + +SCENARIO("A command has a handler", test_tags) +{ + GIVEN ("A command object with a handler") { + auto handler_called = false; + auto cmd = nihil::command("foo bar baz", "foo bar baz -x", [&](int, char **) { + handler_called = true; + return 0; + }); + + THEN ("The usage is correct") { + REQUIRE(cmd.usage() == "foo bar baz -x"); + } + + AND_WHEN ("The command is invoked") { + auto ret = cmd.invoke(0, nullptr); + + THEN ("The return value is 0") { + REQUIRE(ret.value() == 0); + } + AND_THEN ("The handler was called") { + REQUIRE(handler_called == true); + } + } + } +} +} // namespace diff --git a/nihil.cli/command_node.cc b/nihil.cli/command_node.cc deleted file mode 100644 index 5936b8c..0000000 --- a/nihil.cli/command_node.cc +++ /dev/null @@ -1,40 +0,0 @@ -// This source code is released into the public domain. -module; - -#include // _exit - -module nihil.cli; - -import nihil.std; -import nihil.core; -import nihil.error; - -namespace nihil { - -//NOLINTNEXTLINE(bugprone-exception-escape) -command_node::command_node(std::string_view path) noexcept -try : m_path(path) -{ -} catch (std::exception const &exc) { - std::print(std::cerr, "%s\n", exc.what()); - _exit(1); - /*NOTREACHED*/ -} - -command_node::~command_node() = default; - -auto command_node::path(this command_node const &self) noexcept - -> std::string_view -{ - return self.m_path; -} - -auto command_node::invoke(int, char **) const - -> std::expected -{ - // If invoke() wasn't overridden, then this is an empty node, - // so the command was incomplete. - return std::unexpected(error(errc::incomplete_command)); -} - -} // namespace nihil diff --git a/nihil.cli/command_node.ccm b/nihil.cli/command_node.ccm deleted file mode 100644 index 25b5006..0000000 --- a/nihil.cli/command_node.ccm +++ /dev/null @@ -1,26 +0,0 @@ -// This source code is released into the public domain. -export module nihil.cli:command_node; - -// command_node represents a possibly-invocable command. - -import nihil.std; -import nihil.error; - -namespace nihil { - -export struct command_node { - command_node(std::string_view path) noexcept; - - virtual ~command_node(); - - [[nodiscard]] auto path(this command_node const &) noexcept - -> std::string_view; - - [[nodiscard]] virtual auto invoke(int argc, char **argv) const - -> std::expected; - -private: - std::string m_path; -}; - -} // namespace nihil diff --git a/nihil.cli/command_tree.cc b/nihil.cli/command_tree.cc deleted file mode 100644 index a77131a..0000000 --- a/nihil.cli/command_tree.cc +++ /dev/null @@ -1,169 +0,0 @@ -// This source code is released into the public domain. -module nihil.cli; - -import nihil.std; - -namespace nihil { - -command_tree_node::command_tree_node() - : m_this_word("") - , m_command(std::make_shared(command_node(""))) -{ -} - -command_tree_node::command_tree_node(command_tree_node *parent, - std::string_view this_word) - : m_parent(parent) - , m_this_word(this_word) -{ -} - -command_tree_node::command_tree_node(command_tree_node *parent, - std::string_view this_word, - std::shared_ptr command) - : m_parent(parent) - , m_this_word(this_word) - , m_command(std::move(command)) -{ -} - -auto command_tree_node::get_child(this command_tree_node const &self, - std::string_view child) - -> command_tree_node const * -{ - if (auto it = self.m_children.find(std::string(child)); - it != self.m_children.end()) - return &it->second; - - return nullptr; -} - -auto command_tree_node::get_child(this command_tree_node &self, - std::string_view child) - -> command_tree_node * -{ - if (auto it = self.m_children.find(std::string(child)); - it != self.m_children.end()) - return &it->second; - - return nullptr; -} - -auto command_tree_node::get_or_create_child(this command_tree_node &self, - std::string_view child) - -> command_tree_node * -{ - // Return the existing child, if there is one. - if (auto *ptr = self.get_child(child); ptr != nullptr) - return ptr; - - // Insert a new child. - auto [it, ok] = self.m_children.emplace( - child, - command_tree_node(&self, child)); - - // Give the child a dummy command. - auto path = self.m_parent != nullptr - ? std::format("{} {}", self.m_parent->path(), child) - : std::string(child); - - it->second.m_command = std::make_shared(path); - - return &it->second; -} - -auto command_tree_node::command(this command_tree_node const &self) - -> std::shared_ptr const & -{ - return self.m_command; -} - -auto command_tree_node::command(this command_tree_node &self, - std::shared_ptr command) - -> void -{ - // TODO: Put this check back without tripping from the dummy command. - //if (self.m_command != nullptr) - // throw std::logic_error("duplicate command"); - self.m_command = std::move(command); -} - -auto command_tree_node::print_commands(this command_tree_node const &self) - -> void -{ - auto prefix = std::string(self.path()); - - for (auto &&[name, node] : self.m_children) { - auto command = prefix.empty() - ? name - : (prefix + ' ' + name); - std::print(std::cerr, " {}\n", command); - } -} - -auto command_tree_node::path(this command_tree_node const &self) - -> std::string_view -{ - return self.m_command->path(); -} - -auto command_tree::insert(this command_tree &self, - std::vector const &path, - std::shared_ptr command) - -> void -{ - auto *this_node = &self.m_root_node; - - // Find the node for this key. - for (auto &&this_word : path) - this_node = this_node->get_or_create_child(this_word); - - // Set the new value. - this_node->command(std::move(command)); -} - -auto command_tree::find(this command_tree const &self, int &argc, char **&argv) - -> command_tree_node const * -{ - auto const *this_node = &self.m_root_node; - - // Iterate until we don't find a child command, then return that node. - while (argv[0] != nullptr) { - auto const *next_node = this_node->get_child(argv[0]); - - if (next_node == nullptr) - return this_node; - - this_node = next_node; - - --argc; - ++argv; - } - - // We ran out of path without finding a valid command. Return this - // node; the caller will notice the missing command. - return this_node; -} - -auto build_command_tree() -> command_tree -{ - auto const &commands = get_registered_commands(); - auto tree = command_tree(); - - for (auto &&command : commands) { - auto split_path = std::vector( - std::from_range, - command->path() - | std::views::split(' ') - | std::views::transform([] (auto &&r) { - return std::string_view(r); - })); - - // Throws std::logic_error on duplicates. - tree.insert(split_path, command); - } - - return tree; -} - -} // namespace nihil diff --git a/nihil.cli/command_tree.ccm b/nihil.cli/command_tree.ccm index 84e4a0d..6eae9e6 100644 --- a/nihil.cli/command_tree.ccm +++ b/nihil.cli/command_tree.ccm @@ -2,96 +2,174 @@ export module nihil.cli:command_tree; import nihil.std; +import nihil.generator; +import nihil.util; import :command; +import :registry; namespace nihil { -/* - * command_tree_node represents a possibly-empty node in the command tree. - * For example, if two commands "add foo" and "add bar" are defined, - * then "add" will be implicitly created as an empty node. - */ -struct command_tree_node final { - command_tree_node(); - - command_tree_node(command_tree_node *parent, - std::string_view this_word); - - command_tree_node(command_tree_node *parent, - std::string_view this_word, - std::shared_ptr command); - - /* - * Return a child node, or NULL if the child doesn't exist. - */ +// command_tree_node represents a node in the command tree. Each node contains a command, which is +// either a real command which can be invoked, or a stub command which acts as a placeholder in the +// tree. For example, if two commands "add foo" and "add bar" are defined, then "add" will be +// implicitly created as a stub command. +struct command_tree_node final +{ + // Create an empty node with no parent. This is used as the root of the tree. + command_tree_node() + : m_command(std::make_shared("")) + { + } + + // Create a node that contains a command and has a parent. + command_tree_node(command_tree_node *parent, std::string_view const this_word, + std::shared_ptr command) + : m_parent(parent) + , m_this_word(this_word) + , m_command(std::move(command)) + { + } + + // Return a child node, or NULL if the child doesn't exist. [[nodiscard]] auto get_child(this command_tree_node const &self, - std::string_view child) - -> command_tree_node const *; - - [[nodiscard]] auto get_child(this command_tree_node &self, - std::string_view child) - -> command_tree_node *; - - /* - * Return a child node if it exists, or insert a new empty node. - */ - [[nodiscard]] auto get_or_create_child(this command_tree_node &self, - std::string_view child) - -> command_tree_node *; - - /* - * Set or get this node's command. - */ - [[nodiscard]] auto command(this command_tree_node const &self) - -> std::shared_ptr const &; - auto command(this command_tree_node &self, - std::shared_ptr) - -> void; - - /* - * Get the path of this command_node. - */ - [[nodiscard]] auto path(this command_tree_node const &self) - -> std::string_view; - - /* - * Print this node's children in a form useful to humans. - */ - auto print_commands(this command_tree_node const &self) -> void; + std::string_view const child) -> command_tree_node const * + { + if (auto it = self.m_children.find(std::string(child)); it != self.m_children.end()) + return &it->second; + + return nullptr; + } + + [[nodiscard]] auto + get_child(this command_tree_node &self, std::string_view const child) -> command_tree_node * + { + if (auto it = self.m_children.find(std::string(child)); it != self.m_children.end()) + return &it->second; + + return nullptr; + } + + // Return a child node if it exists, or insert a new empty node. + [[nodiscard]] auto get_or_create_child(this command_tree_node &self, std::string_view child) + -> command_tree_node * + { + // Return the existing child, if there is one. + if (auto *ptr = self.get_child(child); ptr != nullptr) + return ptr; + + // Create a new node with a dummy command. + auto path = self.m_parent != nullptr + ? std::format("{} {}", self.m_parent->command().path(), child) + : std::string(child); + auto node = command_tree_node(&self, child, std::make_shared(path)); + + auto [it, ok] = self.m_children.emplace(child, node); + if (!ok) + throw std::logic_error("failed to insert command tree node"); + return &it->second; + } + + // Get node's command. + [[nodiscard]] auto command(this command_tree_node const &self) -> command & + { + return *self.m_command; + } + + auto command(this command_tree_node &self, std::shared_ptr command) -> void + { + // If we have a stub command, allow it to be replaced. This happens when + // a stub node is replaced with a real node during tree building. However, + // if we already have an invocable command, this is an error. + if (self.m_command->invocable()) + throw std::logic_error("duplicate command"); + + self.m_command = std::move(command); + } + + // Yield all children of this node, depth-first. + auto + all_children(this command_tree_node const &self) -> generator + { + for (auto &&node : self.m_children | std::views::values) { + co_yield node; + co_yield elements_of(node.all_children()); + } + } + + // Yield direct children in this node. + auto children(this command_tree_node const &self) + { + return self.m_children | std::views::values; + } private: - command_tree_node *m_parent = nullptr; - std::string m_this_word; - std::shared_ptr m_command; - std::map - m_children; + command_tree_node *m_parent = nullptr; + std::string m_this_word; + std::shared_ptr m_command; + std::map m_children; }; -/* - * The command tree stores commands in a tree structure suitable for searching. - */ -struct command_tree { - /* - * Add a node to the tree. Returns false if the node already exists. - */ - auto insert(this command_tree &self, - std::vector const &path, - std::shared_ptr command) - -> void; - - /* - * Find a node in the tree. - */ - auto find(this command_tree const &self, int &argc, char **&argv) - -> command_tree_node const *; +// The command tree stores commands in a tree structure suitable for searching. +struct command_tree +{ + // Add a node to the tree. Returns false if the node already exists. + auto insert(this command_tree &self, std::vector const &path, + std::shared_ptr command) -> void + { + auto *this_node = &self.m_root_node; + + // Find the node for this key. + for (auto &&this_word : path) + this_node = this_node->get_or_create_child(this_word); + + // Set the new value. + this_node->command(std::move(command)); + } + + // Find a node in the tree. + [[nodiscard]] auto + find(this command_tree const &self, int &argc, char **&argv) -> command_tree_node const * + { + auto const *this_node = &self.m_root_node; + + // Iterate until we don't find a child command, then return that node. + while (argv[0] != nullptr) { + auto const *next_node = this_node->get_child(argv[0]); + + if (next_node == nullptr) + return this_node; + + this_node = next_node; + + --argc; + ++argv; + } + + // We ran out of path without finding a valid command. Return this + // node; the caller will notice the missing command. + return this_node; + } private: command_tree_node m_root_node; }; -/* - * Build a command tree from the registry. - */ -[[nodiscard]] auto build_command_tree() -> command_tree; +// Build a command tree from the registry. +[[nodiscard]] auto build_command_tree() -> command_tree +{ + auto tree = command_tree(); + + for (auto &&command : get_registered_commands()) { + auto split_path = command->path() // + | std::views::split(' ') // + | std::views::transform(construct) // + | std::ranges::to(); + + // Throws std::logic_error on duplicates. + tree.insert(split_path, command); + } + + return tree; +} } // namespace nihil diff --git a/nihil.cli/dispatch_command.cc b/nihil.cli/dispatch_command.cc index 7b4cf39..6cf7f05 100644 --- a/nihil.cli/dispatch_command.cc +++ b/nihil.cli/dispatch_command.cc @@ -2,6 +2,7 @@ module; #include // getprogname, NOLINT +#include // EX_USAGE #include // getopt module nihil.cli; @@ -11,6 +12,15 @@ import nihil.core; namespace nihil { +namespace { +// Print this node's children in a form useful to humans. +auto print_commands(command_tree_node const &node) -> void +{ + for (auto &&child : node.children()) + std::print(std::cerr, " {}\n", child.command().path()); +} +} + auto dispatch_command(int argc, char **argv) -> int { auto tree = build_command_tree(); @@ -20,52 +30,44 @@ auto dispatch_command(int argc, char **argv) -> int // argv[0] will be set to something reasonable for the next call // to getopt(). - // find() never returns nullptr; at worst it will return the - // root node. + // find() never returns nullptr; at worst it will return the root node. auto const *node = tree.find(argc, argv); - - // Get the command_node. auto const &command = node->command(); // Reset getopt(3) for the command, in case main() used it already. optreset = 1; optind = 1; - /* - * Set the program name to the existing progname plus the full path - * to the command being invoked; this makes error messages nicer. - */ + // Set the program name to the existing progname plus the full path to the command being + // invoked; this makes error messages nicer. Save the old progname so we can restore it + // after invoking the command. auto const *old_progname = ::getprogname(); { auto cprogname = std::format("{} {}", ::getprogname(), - command->path()); + command.path()); ::setprogname(cprogname.c_str()); } - // Invoke the command see what it returns. - auto ret = command->invoke(argc, argv); + // Invoke the command see what it returns. If it's an exit code, just return it. + // Otherwise, handle the error. + auto ret = command.invoke(argc, argv); // Restore the old progname. ::setprogname(old_progname); - // If the command produced an exit code, return it. if (ret) return *ret; - /* - * We have special handling for some errors. - */ - // Incomplete command: print the list of valid commands at this node. if (ret.error() == errc::incomplete_command) { - std::print(std::cerr, "{}: usage:\n", ::getprogname()); - node->print_commands(); - return 1; + std::println(std::cerr, "{}: usage:", ::getprogname()); + print_commands(*node); + return EX_USAGE; } // We didn't recognise the error, so just print it and exit. - std::print(std::cerr, "{}\n", ret.error()); + std::println(std::cerr, "{}", ret.error()); return 1; } diff --git a/nihil.cli/nihil.cli.ccm b/nihil.cli/nihil.cli.ccm index 6d98c05..5463fd9 100644 --- a/nihil.cli/nihil.cli.ccm +++ b/nihil.cli/nihil.cli.ccm @@ -1,14 +1,8 @@ -/* - * This source code is released into the public domain. - */ - -module; - +// This source code is released into the public domain. export module nihil.cli; export import :command; export import :command_tree; -export import :command_node; export import :dispatch_command; export import :registry; export import :usage_error; diff --git a/nihil.cli/registry.cc b/nihil.cli/registry.cc index 0f0041b..a972a65 100644 --- a/nihil.cli/registry.cc +++ b/nihil.cli/registry.cc @@ -5,13 +5,11 @@ import nihil.std; namespace nihil { -/* - * Get the registry storage. Because this is called from global ctors, - * it handles exceptions itself. - */ -auto get_registry() noexcept -> std::vector> & +// Get the registry storage. Because this is called from global ctors, +// it handles exceptions itself. +auto get_registry() noexcept -> std::vector> & try { - static auto commands = std::vector>(); + static auto commands = std::vector>(); return commands; } catch (std::exception const &exc) { std::println(std::cerr, "{}", exc.what()); @@ -21,12 +19,10 @@ try { std::exit(1); // NOLINT } -/* - * Register a new command. - */ +// Register a new command. auto register_command(command *cmd) noexcept -> void try { - auto null_deleter = [] (command_node const *) -> void {}; + auto null_deleter = [] (command const *) -> void {}; auto &commands = get_registry(); commands.emplace_back(cmd, null_deleter); @@ -38,10 +34,8 @@ try { std::exit(1); // NOLINT } -/* - * Get the list of registered commands. - */ -auto get_registered_commands() -> std::span> +// Get the list of registered commands. +auto get_registered_commands() -> std::span> { return {get_registry()}; } diff --git a/nihil.cli/registry.ccm b/nihil.cli/registry.ccm index 673f516..e5a7e29 100644 --- a/nihil.cli/registry.ccm +++ b/nihil.cli/registry.ccm @@ -6,13 +6,12 @@ import nihil.std; namespace nihil { export struct command; -export struct command_node; // Register a command. This is guaranteed not to throw; errors will print // a diagnostic and exit. auto register_command(command *cmd) noexcept -> void; // Get previously registered commands. -auto get_registered_commands() -> std::span>; +auto get_registered_commands() -> std::span>; } // namespace nihil diff --git a/nihil.cli/test.cc b/nihil.cli/test.cc index 61d164d..8253bdc 100644 --- a/nihil.cli/test.cc +++ b/nihil.cli/test.cc @@ -1,5 +1,7 @@ // This source code is released into the public domain. +#include + #include import nihil.std; @@ -69,7 +71,7 @@ TEST_CASE("nihil.cli: dispatch_command: unknown command", "[nihil.cli]") output = capture.str(); } - REQUIRE(ret == 1); + REQUIRE(ret == EX_USAGE); auto const *progname = ::getprogname(); REQUIRE(output == std::format("{}: usage:\n cmd\n", progname)); @@ -92,7 +94,7 @@ TEST_CASE("nihil.cli: dispatch_command: incomplete command", "[nihil.cli]") output = capture.str(); } - REQUIRE(ret == 1); + REQUIRE(ret == EX_USAGE); auto const *progname = ::getprogname(); REQUIRE(output == std::format("{}: usage:\n cmd sub1\n cmd sub2\n", -- cgit v1.2.3