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/command_map.cc | 267 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 nihil.cli/command_map.cc (limited to 'nihil.cli/command_map.cc') 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 -- cgit v1.2.3