diff options
Diffstat (limited to 'nihil.cli')
| -rw-r--r-- | nihil.cli/CMakeLists.txt | 14 | ||||
| -rw-r--r-- | nihil.cli/command.cc | 51 | ||||
| -rw-r--r-- | nihil.cli/command.ccm | 48 | ||||
| -rw-r--r-- | nihil.cli/command_map.cc | 267 | ||||
| -rw-r--r-- | nihil.cli/command_map.ccm | 67 | ||||
| -rw-r--r-- | nihil.cli/command_node.cc | 42 | ||||
| -rw-r--r-- | nihil.cli/command_node.ccm | 35 | ||||
| -rw-r--r-- | nihil.cli/command_path.ccm | 16 | ||||
| -rw-r--r-- | nihil.cli/command_tree.cc | 187 | ||||
| -rw-r--r-- | nihil.cli/command_tree.ccm | 107 | ||||
| -rw-r--r-- | nihil.cli/dispatch_command.cc | 82 | ||||
| -rw-r--r-- | nihil.cli/dispatch_command.ccm | 31 | ||||
| -rw-r--r-- | nihil.cli/nihil.cli.ccm | 7 | ||||
| -rw-r--r-- | nihil.cli/registry.cc | 57 | ||||
| -rw-r--r-- | nihil.cli/registry.ccm | 28 | ||||
| -rw-r--r-- | nihil.cli/test.cc | 100 | ||||
| -rw-r--r-- | nihil.cli/test_command_map.cc | 32 | ||||
| -rw-r--r-- | nihil.cli/usage_error.ccm | 2 |
18 files changed, 801 insertions, 372 deletions
diff --git a/nihil.cli/CMakeLists.txt b/nihil.cli/CMakeLists.txt index a677798..b05ed3a 100644 --- a/nihil.cli/CMakeLists.txt +++ b/nihil.cli/CMakeLists.txt @@ -6,18 +6,26 @@ target_sources(nihil.cli PUBLIC FILE_SET modules TYPE CXX_MODULES FILES nihil.cli.ccm - command_map.ccm + command.ccm + command_tree.ccm + command_node.ccm + dispatch_command.ccm + registry.ccm usage_error.ccm PRIVATE - command_map.cc + command.cc + command_tree.cc + command_node.cc + dispatch_command.cc + registry.cc ) if(NIHIL_TESTS) enable_testing() add_executable(nihil.cli.test - test_command_map.cc + test.cc ) target_link_libraries(nihil.cli.test PRIVATE nihil.cli diff --git a/nihil.cli/command.cc b/nihil.cli/command.cc new file mode 100644 index 0000000..475cad0 --- /dev/null +++ b/nihil.cli/command.cc @@ -0,0 +1,51 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <functional> +#include <iostream> +#include <print> +#include <string> + +// For EX_USAGE. While <sysexits.h> 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 <sysexits.h> + +module nihil.cli; + +import nihil.error; +import :registry; + +namespace nihil { + +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<int, error> +{ + 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 new file mode 100644 index 0000000..74ef030 --- /dev/null +++ b/nihil.cli/command.ccm @@ -0,0 +1,48 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <functional> +#include <string> + +export module nihil.cli:command; + +import nihil.error; +import :command_node; + +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. + */ + +using command_handler_t = int (int, char **); +using command_function_t = std::function<command_handler_t>; + +export struct command final : command_node { + command(std::string_view path, std::string_view usage, + command_function_t); + + command(std::string_view path, std::string_view usage, auto &&fn) + : command(path, usage, command_function_t(fn)) + {} + + [[nodiscard]] auto usage(this command const &) noexcept + -> std::string_view; + + [[nodiscard]] auto invoke(int argc, char **argv) const + -> std::expected<int, error> override; + +private: + std::string_view m_path; + std::string_view m_usage; + command_function_t m_handler; +}; + +} // namespace nihil diff --git a/nihil.cli/command_map.cc b/nihil.cli/command_map.cc deleted file mode 100644 index c656c62..0000000 --- a/nihil.cli/command_map.cc +++ /dev/null @@ -1,267 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include <cstdio> -#include <functional> -#include <iostream> -#include <map> -#include <ranges> -#include <string> -#include <utility> - -#include <unistd.h> - -module nihil.cli; - -/* - * command_map represents a hierarchical list of commands. At each level, - * a command is mapped to a handler, which can either be a function, in - * which case we execute the function, or another command_map, in which - * case we invoke the new map - */ - -namespace nihil { - -/* - * The string tree we store our commands in. This is sort of like a very - * basic hierarchical std::map. Keys are provided as a range of values, - * typically from argv. - */ - -struct command_tree_node final { - command_tree_node() - : _this_word("") - { - } - - command_tree_node(std::string_view this_word) - : _this_word(this_word) - { - } - - command_tree_node(std::string_view this_word, - command value) - : _this_word(this_word) - , _value(std::move(value)) - { - } - - /* - * Return a child node, or NULL if the child doesn't exist. - */ - auto get_child(this command_tree_node const &self, - std::string_view child) - -> command_tree_node const * - { - if (auto it = self.children.find(std::string(child)); - it != self.children.end()) - return &it->second; - - return nullptr; - } - - auto get_child(this command_tree_node &self, - std::string_view child) - -> command_tree_node * - { - if (auto it = self.children.find(std::string(child)); - it != self.children.end()) - return &it->second; - - return nullptr; - } - - /* - * Return a child node if it exists, or insert a new empty node. - */ - auto get_or_create_child(this command_tree_node &self, - std::string_view child) - -> command_tree_node * - { - if (auto ptr = self.get_child(child); ptr != nullptr) - return ptr; - - auto [it, ok] = self.children.emplace( - child, command_tree_node(child)); - return &it->second; - } - - /* - * Return this node's value. - */ - auto value(this command_tree_node const &self) - -> std::optional<command> const & - { - return self._value; - } - - /* - * Set this node's value. - */ - auto value(this command_tree_node &self, command value) -> void - { - self._value = std::move(value); - } - - /* - * Print this node's children in a form useful to humans. - */ - auto print_commands(this command_tree_node const &self, - std::string_view prefix) -> void - { - for (auto &&[name, node] : self.children) { - std::print(" {} {}\n", prefix, name); - } - } - -private: - std::string _this_word; - std::optional<command> _value; - std::unordered_map<std::string, command_tree_node> children; -}; - -struct command_tree { - /* - * Add a node to the tree. Returns false if the node already exists. - */ - auto insert(this command_tree &self, - std::ranges::range auto &&path, - command value) - -> bool - { - auto *this_node = &self._root_node; - - // Find the node for this key. - for (auto &&next : path) { - auto this_word = std::string_view(next); - this_node = this_node->get_or_create_child(this_word); - } - - if (this_node->value()) { - // The value already exists. - return false; - } - - // Set the new value. - this_node->value(std::move(value)); - return true; - } - - /* - * Find a node in the tree. Unlike insert(), iteration stops when - * we find any node with a value. - */ - auto find(this command_tree const &self, int &argc, char **&argv) - -> std::optional<command> - { - auto *this_node = &self._root_node; - - // Assume the caller already stripped the program name from - // argv. This is usually the case since they would have - // called getopt(). - - // Store the command bits we got so far, so we can print them - // in the usage error if needed. - auto path = std::string(); - - while (argv[0] != nullptr) { - auto next = std::string_view(argv[0]); - - auto *next_node = this_node->get_child(next); - - if (next_node == nullptr) { - // The node doesn't exist, so this command is - // not valid. Print a list of valid commands. - std::print(std::cerr, - "{}: unknown command: {} {}\n", - ::getprogname(), path, next); - std::print(std::cerr, - "{}: expected one of:\n", - ::getprogname()); - - this_node->print_commands(path); - return {}; - } - - this_node = next_node; - - if (this_node->value()) - // This node has a value, so return it. - return {this_node->value()}; - - if (!path.empty()) - path += ' '; - path += next; - --argc; - ++argv; - } - - // We didn't find a value, so the key was incomplete. - std::print(std::cerr, "{}: {} command; expected:\n", - ::getprogname(), - path.empty() ? "missing" : "incomplete"); - this_node->print_commands(path); - - return {}; - } - -private: - command_tree_node _root_node; -}; - -/* - * The global command map. - */ -auto get_commands() -> command_tree & { - static auto commands = command_tree(); - return commands; -} - -auto register_command(std::string_view path, command *cmd) noexcept -> void -try { - auto &commands = get_commands(); - if (commands.insert(path | std::views::split(' '), *cmd) == false) { - std::printf("command registration failed\n"); - std::abort(); - } -} catch (...) { - std::printf("command registration failed\n"); - std::abort(); -} - -auto dispatch_command(int argc, char **argv) -> int -{ - auto &commands = get_commands(); - - // The caller should have stripped argv[0] already. find() will - // strip all the remaining elements except the last, which means - // argv[0] will be set to something reasonable for the next call - // to getopt(). - - auto node = commands.find(argc, argv); - - if (!node) - // find() already printed the error message - return 1; - - // Reset getopt here for the command to use. - optreset = 1; - optind = 1; - - // Calling setprogname() makes error messages more relevant. - auto cprogname = std::format("{} {}", ::getprogname(), - node->path()); - ::setprogname(cprogname.c_str()); - - return node->invoke(argc, argv); -} - -void print_usage(std::string_view) -{ -// get_root_node().print_usage(std::string(prefix)); -} - -} // namespace nihil diff --git a/nihil.cli/command_map.ccm b/nihil.cli/command_map.ccm deleted file mode 100644 index 8cf9d9c..0000000 --- a/nihil.cli/command_map.ccm +++ /dev/null @@ -1,67 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include <functional> -#include <iostream> -#include <map> -#include <string> -#include <utility> - -export module nihil.cli:command_map; - -import nihil.util; -import :usage_error; - -/* - * command_map represents a hierarchical list of commands. At each level, - * a command is mapped to a handler, which can either be a function, in - * which case we execute the function, or another command_map, in which - * case we invoke the new map - */ - -namespace nihil { - -export struct command; - -/* - * Register a command; used by command<>::command(). - */ -auto register_command(std::string_view path, command *) noexcept -> void; - -/* - * 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. - */ -export struct command { - command(std::string_view path, std::string_view usage, auto &&fn) - : m_path(path) - , m_usage(usage) - , m_handler(std::forward<decltype(fn)>(fn)) - { - register_command(path, this); - } - - [[nodiscard]] auto path(this command const &self) -> std::string_view - { - return self.m_path; - } - - auto invoke(this command const &self, int argc, char **argv) -> int - { - return std::invoke(self.m_handler, argc, argv); - } - -private: - std::string_view m_path; - std::string_view m_usage; - std::function<int (int, char **)> m_handler; -}; - -// The public API. -export [[nodiscard]] auto dispatch_command(int argc, char **argv) -> int; -export auto print_usage(std::string_view prefix) -> void; - -} // namespace nihil diff --git a/nihil.cli/command_node.cc b/nihil.cli/command_node.cc new file mode 100644 index 0000000..98aeac1 --- /dev/null +++ b/nihil.cli/command_node.cc @@ -0,0 +1,42 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <iostream> +#include <print> +#include <string> + +module nihil.cli; + +import nihil.core; +import nihil.error; + +namespace nihil { + +command_node::command_node(std::string_view path) noexcept + : m_path(path) +{ +} + +command_node::~command_node() +{ +} + +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<int, error> +{ + // 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 new file mode 100644 index 0000000..546eb46 --- /dev/null +++ b/nihil.cli/command_node.ccm @@ -0,0 +1,35 @@ +/* + * This source code is released into the public domain. + */ + +module; + +/* + * command_node represents a possibly-invocable command. + */ + +#include <expected> +#include <string> + +export module nihil.cli:command_node; + +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<int, error>; + +private: + std::string m_path; +}; + +} // namespace nihil diff --git a/nihil.cli/command_path.ccm b/nihil.cli/command_path.ccm new file mode 100644 index 0000000..a5bd5e3 --- /dev/null +++ b/nihil.cli/command_path.ccm @@ -0,0 +1,16 @@ +/* + * This source code is released into the public domain. + */ + +module; + +/* + * command_path represents a command split into its component parts. + */ + +export module nihil.cli:command_path; + +namespace nihil { + + +} // namespace nihil diff --git a/nihil.cli/command_tree.cc b/nihil.cli/command_tree.cc new file mode 100644 index 0000000..4142a40 --- /dev/null +++ b/nihil.cli/command_tree.cc @@ -0,0 +1,187 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <iostream> +#include <print> +#include <ranges> +#include <string> +#include <vector> + +module nihil.cli; + +namespace nihil { + +command_tree_node::command_tree_node() + : m_this_word("") + , m_command(std::make_shared<command_node>(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_node> 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. + it->second.m_command = std::make_shared<command_node>( + self.path() + ' ' + child); + + return &it->second; +} + +auto command_tree_node::command(this command_tree_node const &self) + -> std::shared_ptr<command_node> const & +{ + return self.m_command; +} + +auto command_tree_node::command(this command_tree_node &self, + std::shared_ptr<command_node> 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 +{ + auto path = std::string(); + + auto const *node = &self; + while (node->m_parent != nullptr) { + path = node->m_this_word + ' ' + path; + node = node->m_parent; + } + + // Trim the trailing space. + if (!path.empty()) + path.pop_back(); + + return path; +} + +auto command_tree::insert(this command_tree &self, + std::vector<std::string_view> const &path, + std::shared_ptr<command_node> 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 *this_node = &self.m_root_node; + + // Iterate until we don't find a child command, then return that node. + while (argv[0] != nullptr) { + auto *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::string_view>( + 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 new file mode 100644 index 0000000..f52b768 --- /dev/null +++ b/nihil.cli/command_tree.ccm @@ -0,0 +1,107 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <map> +#include <memory> +#include <optional> +#include <ranges> +#include <vector> + +export module nihil.cli:command_tree; + +import :command; + +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_node> 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<command_node> const &; + auto command(this command_tree_node &self, + std::shared_ptr<command_node>) + -> void; + + /* + * Get the path of this command_node. + */ + [[nodiscard]] auto path(this command_tree_node const &self) + -> std::string; + + /* + * Print this node's children in a form useful to humans. + */ + auto print_commands(this command_tree_node const &self) -> void; + +private: + command_tree_node *m_parent = nullptr; + std::string m_this_word; + std::shared_ptr<command_node> m_command; + std::map<std::string, command_tree_node> + 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<std::string_view> const &path, + std::shared_ptr<command_node> command) + -> void; + + /* + * Find a node in the tree. + */ + auto find(this command_tree const &self, int &argc, char **&argv) + -> command_tree_node const *; + +private: + command_tree_node m_root_node; +}; + +/* + * Build a command tree from the registry. + */ +[[nodiscard]] auto build_command_tree() -> command_tree; + +} // namespace nihil diff --git a/nihil.cli/dispatch_command.cc b/nihil.cli/dispatch_command.cc new file mode 100644 index 0000000..736e16e --- /dev/null +++ b/nihil.cli/dispatch_command.cc @@ -0,0 +1,82 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <cstdio> +#include <functional> +#include <iostream> +#include <map> +#include <print> +#include <ranges> +#include <string> +#include <utility> + +#include <unistd.h> + +module nihil.cli; + +import nihil.core; + +namespace nihil { + +auto dispatch_command(int argc, char **argv) -> int +{ + auto tree = build_command_tree(); + + // The caller should have stripped argv[0] already. find() will + // strip all the remaining elements except the last, which means + // 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. + 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. + */ + auto *old_progname = ::getprogname(); + + { + auto cprogname = std::format("{} {}", ::getprogname(), + command->path()); + ::setprogname(cprogname.c_str()); + } + + // Invoke the command see what it returns. + 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; + } + + // We didn't recognise the error, so just print it and exit. + std::print(std::cerr, "{}\n", ret.error()); + return 1; +} + +} // namespace nihil diff --git a/nihil.cli/dispatch_command.ccm b/nihil.cli/dispatch_command.ccm new file mode 100644 index 0000000..1ba55bb --- /dev/null +++ b/nihil.cli/dispatch_command.ccm @@ -0,0 +1,31 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <functional> +#include <iostream> +#include <map> +#include <string> +#include <utility> + +export module nihil.cli:dispatch_command; + +import nihil.util; +import :command; +import :usage_error; + +namespace nihil { + +/* + * Invoke a command (which must have been previously registered) using + * the provided argument vector. + * + * The caller should have already stripped the executable name from argv[0] + * so that the vector starts with the command name. This is implicitly + * done if main() uses getopt(). + */ +export [[nodiscard]] auto dispatch_command(int argc, char **argv) -> int; + +} // namespace nihil diff --git a/nihil.cli/nihil.cli.ccm b/nihil.cli/nihil.cli.ccm index faeb44e..6d98c05 100644 --- a/nihil.cli/nihil.cli.ccm +++ b/nihil.cli/nihil.cli.ccm @@ -6,4 +6,9 @@ module; export module nihil.cli; -export import :command_map; +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 new file mode 100644 index 0000000..e35078d --- /dev/null +++ b/nihil.cli/registry.cc @@ -0,0 +1,57 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <cstdio> +#include <exception> +#include <memory> +#include <vector> + +module nihil.cli; + +namespace nihil { + +/* + * Get the registry storage. Because this is called from global ctors, + * it handles exceptions itself. + */ +auto get_registry() noexcept -> std::vector<std::shared_ptr<command_node>> & +try { + static auto commands = std::vector<std::shared_ptr<command_node>>(); + return commands; +} catch (std::exception const &exc) { + std::printf("%s\n", exc.what()); + std::exit(1); +} catch (...) { + std::printf("get_registered_commands(): unknown error\n"); + std::exit(1); +} + +/* + * Register a new command. + */ +auto register_command(command *cmd) noexcept -> void +try { + auto null_deleter = [] (command_node const *) -> void {}; + + auto &commands = get_registry(); + commands.emplace_back(cmd, null_deleter); +} catch (std::exception const &exc) { + std::printf("%s\n", exc.what()); + std::exit(1); +} catch (...) { + std::printf("get_registered_commands(): unknown error\n"); + std::exit(1); +} + +/* + * Get the list of registered commands. + */ +auto get_registered_commands() -> std::span<std::shared_ptr<command_node>> +{ + return {get_registry()}; +} + +} // namespace nihil diff --git a/nihil.cli/registry.ccm b/nihil.cli/registry.ccm new file mode 100644 index 0000000..0b9754d --- /dev/null +++ b/nihil.cli/registry.ccm @@ -0,0 +1,28 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <memory> +#include <span> + +export module nihil.cli:registry; + +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<std::shared_ptr<command_node>>; + +} // namespace nihil diff --git a/nihil.cli/test.cc b/nihil.cli/test.cc new file mode 100644 index 0000000..c265700 --- /dev/null +++ b/nihil.cli/test.cc @@ -0,0 +1,100 @@ +/* + * This source code is released into the public domain. + */ + +#include <iostream> +#include <vector> + +#include <catch2/catch_test_macros.hpp> + +import nihil.cli; +import nihil.util; + +namespace { + +auto cmd_sub1_called = false; +auto cmd_sub1 = nihil::command("cmd sub1", "", [](int, char **) -> int +{ + cmd_sub1_called = true; + return 0; +}); + +auto cmd_sub2_called = false; +auto cmd_sub2 = nihil::command("cmd sub2", "", [](int, char **) -> int +{ + cmd_sub2_called = true; + return 0; +}); + +} // anonymous namespace + +TEST_CASE("nihil.cli: dispatch_command: basic", "[nihil.cli]") +{ + SECTION("cmd sub1") { + auto args = std::vector<char const *>{ + "cmd", "sub1", nullptr + }; + auto argv = const_cast<char **>(args.data()); + + int ret = nihil::dispatch_command(args.size() - 1, argv); + REQUIRE(ret == 0); + REQUIRE(cmd_sub1_called == true); + REQUIRE(cmd_sub2_called == false); + } + + SECTION("cmd sub2") { + auto args = std::vector<char const *>{ + "cmd", "sub2", nullptr + }; + auto argv = const_cast<char **>(args.data()); + + int ret = nihil::dispatch_command(args.size() - 1, argv); + REQUIRE(ret == 0); + REQUIRE(cmd_sub2_called == true); + } +} + +TEST_CASE("nihil.cli: dispatch_command: unknown command", "[nihil.cli]") +{ + auto args = std::vector<char const *>{ + "nocomd", "sub", nullptr + }; + auto argv = const_cast<char **>(args.data()); + + auto output = std::string(); + auto ret = int{}; + { + auto capture = nihil::capture_stream(std::cerr); + ret = nihil::dispatch_command(args.size() - 1, argv); + std::cerr.flush(); + output = capture.str(); + } + + REQUIRE(ret == 1); + + auto *progname = ::getprogname(); + REQUIRE(output == std::format("{}: usage:\n cmd\n", progname)); +} + +TEST_CASE("nihil.cli: dispatch_command: incomplete command", "[nihil.cli]") +{ + auto args = std::vector<char const *>{ + "cmd", nullptr + }; + auto argv = const_cast<char **>(args.data()); + + auto output = std::string(); + auto ret = int{}; + { + auto capture = nihil::capture_stream(std::cerr); + ret = nihil::dispatch_command(args.size() - 1, argv); + std::cerr.flush(); + output = capture.str(); + } + + REQUIRE(ret == 1); + + auto *progname = ::getprogname(); + REQUIRE(output == std::format("{}: usage:\n cmd sub1\n cmd sub2\n", + progname)); +} diff --git a/nihil.cli/test_command_map.cc b/nihil.cli/test_command_map.cc deleted file mode 100644 index 1b87a49..0000000 --- a/nihil.cli/test_command_map.cc +++ /dev/null @@ -1,32 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include <vector> - -#include <catch2/catch_test_macros.hpp> - -import nihil.cli; - -namespace { - -auto cmd_sub1_called = false; -auto cmd_sub1 = nihil::command("cmd sub1", "", [](int, char **) -> int -{ - cmd_sub1_called = true; - return 0; -}); - -} // anonymous namespace - -TEST_CASE("command_map: basic", "[command_map]") -{ - auto args = std::vector<char const *>{ - "cmd", "sub1", nullptr - }; - auto argv = const_cast<char **>(args.data()); - - int ret = nihil::dispatch_command(args.size() - 1, argv); - REQUIRE(ret == 0); - REQUIRE(cmd_sub1_called == true); -} diff --git a/nihil.cli/usage_error.ccm b/nihil.cli/usage_error.ccm index 11646e6..61feba7 100644 --- a/nihil.cli/usage_error.ccm +++ b/nihil.cli/usage_error.ccm @@ -20,5 +20,3 @@ export struct usage_error : error { }; } // namespace nihil - - |
