From a2d7181700ac64b8e7a4472ec26dfa253b38f188 Mon Sep 17 00:00:00 2001 From: Lexi Winter Date: Sat, 28 Jun 2025 19:25:55 +0100 Subject: split nihil into separate modules --- nihil.cli/CMakeLists.txt | 32 +++++ nihil.cli/command_map.cc | 267 ++++++++++++++++++++++++++++++++++++++++++ nihil.cli/command_map.ccm | 67 +++++++++++ nihil.cli/nihil.cli.ccm | 9 ++ nihil.cli/test_command_map.cc | 32 +++++ nihil.cli/usage_error.ccm | 24 ++++ 6 files changed, 431 insertions(+) create mode 100644 nihil.cli/CMakeLists.txt create mode 100644 nihil.cli/command_map.cc create mode 100644 nihil.cli/command_map.ccm create mode 100644 nihil.cli/nihil.cli.ccm create mode 100644 nihil.cli/test_command_map.cc create mode 100644 nihil.cli/usage_error.ccm (limited to 'nihil.cli') diff --git a/nihil.cli/CMakeLists.txt b/nihil.cli/CMakeLists.txt new file mode 100644 index 0000000..a677798 --- /dev/null +++ b/nihil.cli/CMakeLists.txt @@ -0,0 +1,32 @@ +# This source code is released into the public domain. + +add_library(nihil.cli STATIC) +target_link_libraries(nihil.cli PRIVATE nihil.util) +target_sources(nihil.cli + PUBLIC FILE_SET modules TYPE CXX_MODULES FILES + nihil.cli.ccm + + command_map.ccm + usage_error.ccm + + PRIVATE + command_map.cc +) + +if(NIHIL_TESTS) + enable_testing() + + add_executable(nihil.cli.test + test_command_map.cc + ) + target_link_libraries(nihil.cli.test PRIVATE + nihil.cli + Catch2::Catch2WithMain + ) + + find_package(Catch2 REQUIRED) + + include(CTest) + include(Catch) + catch_discover_tests(nihil.cli.test) +endif() diff --git a/nihil.cli/command_map.cc b/nihil.cli/command_map.cc new file mode 100644 index 0000000..c656c62 --- /dev/null +++ b/nihil.cli/command_map.cc @@ -0,0 +1,267 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include +#include +#include + +#include + +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 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 _value; + std::unordered_map 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 + { + 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 new file mode 100644 index 0000000..8cf9d9c --- /dev/null +++ b/nihil.cli/command_map.ccm @@ -0,0 +1,67 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include + +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(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 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/nihil.cli.ccm b/nihil.cli/nihil.cli.ccm new file mode 100644 index 0000000..faeb44e --- /dev/null +++ b/nihil.cli/nihil.cli.ccm @@ -0,0 +1,9 @@ +/* + * This source code is released into the public domain. + */ + +module; + +export module nihil.cli; + +export import :command_map; diff --git a/nihil.cli/test_command_map.cc b/nihil.cli/test_command_map.cc new file mode 100644 index 0000000..1b87a49 --- /dev/null +++ b/nihil.cli/test_command_map.cc @@ -0,0 +1,32 @@ +/* + * This source code is released into the public domain. + */ + +#include + +#include + +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{ + "cmd", "sub1", nullptr + }; + auto argv = const_cast(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 new file mode 100644 index 0000000..11646e6 --- /dev/null +++ b/nihil.cli/usage_error.ccm @@ -0,0 +1,24 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include + +export module nihil.cli:usage_error; + +import nihil.error; + +namespace nihil { + +/* + * Exception thrown to indicate invalid command-line arguments. + */ +export struct usage_error : error { + usage_error(std::string_view what) : error(what) {} +}; + +} // namespace nihil + + -- cgit v1.2.3