aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.cli
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-06-28 19:25:55 +0100
committerLexi Winter <lexi@le-fay.org>2025-06-28 19:25:55 +0100
commita2d7181700ac64b8e7a4472ec26dfa253b38f188 (patch)
tree23c5a9c8ec4089ac346e2e0f9391909c3089b66b /nihil.cli
parentf226d46ee02b57dd76a4793593aa8d66e1c58353 (diff)
downloadnihil-a2d7181700ac64b8e7a4472ec26dfa253b38f188.tar.gz
nihil-a2d7181700ac64b8e7a4472ec26dfa253b38f188.tar.bz2
split nihil into separate modules
Diffstat (limited to 'nihil.cli')
-rw-r--r--nihil.cli/CMakeLists.txt32
-rw-r--r--nihil.cli/command_map.cc267
-rw-r--r--nihil.cli/command_map.ccm67
-rw-r--r--nihil.cli/nihil.cli.ccm9
-rw-r--r--nihil.cli/test_command_map.cc32
-rw-r--r--nihil.cli/usage_error.ccm24
6 files changed, 431 insertions, 0 deletions
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 <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
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 <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/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 <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
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 <string>
+
+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
+
+