aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--nihil/CMakeLists.txt6
-rw-r--r--nihil/command_map.ccm153
-rw-r--r--nihil/next_word.ccm50
-rw-r--r--nihil/nihil.ccm4
-rw-r--r--nihil/skipws.ccm40
-rw-r--r--nihil/usage_error.ccm28
-rw-r--r--tests/CMakeLists.txt3
-rw-r--r--tests/command_map.cc41
-rw-r--r--tests/next_word.cc65
-rw-r--r--tests/skipws.cc45
10 files changed, 434 insertions, 1 deletions
diff --git a/nihil/CMakeLists.txt b/nihil/CMakeLists.txt
index 383dd0a..1e9962b 100644
--- a/nihil/CMakeLists.txt
+++ b/nihil/CMakeLists.txt
@@ -4,10 +4,14 @@ add_library(nihil STATIC)
target_sources(nihil PUBLIC
FILE_SET modules TYPE CXX_MODULES FILES
nihil.ccm
+ command_map.ccm
ctype.ccm
fd.ccm
generator.ccm
generic_error.ccm
getenv.ccm
guard.ccm
- tabulate.ccm)
+ next_word.ccm
+ skipws.ccm
+ tabulate.ccm
+ usage_error.ccm)
diff --git a/nihil/command_map.ccm b/nihil/command_map.ccm
new file mode 100644
index 0000000..5aeb7c8
--- /dev/null
+++ b/nihil/command_map.ccm
@@ -0,0 +1,153 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <functional>
+#include <iostream>
+#include <map>
+#include <string>
+#include <utility>
+
+export module nihil:command_map;
+
+import :next_word;
+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 {
+
+template<typename Context>
+struct node;
+
+template<typename Context>
+auto get_root_node() noexcept -> node<Context> &;
+
+// Declare a global command and add it to the root node.
+export template<typename Context>
+struct command final {
+ template<typename F>
+ command(std::string_view path, F fn)
+ try : _func(std::move(fn))
+ {
+ auto &node = get_root_node<Context>().create_node(path);
+ node.handler = this;
+ } catch (std::exception const &exc) {
+ std::cerr << "ERROR: failed to initialise command "
+ << path << ": " << exc.what() << "\n";
+ std::abort();
+ }
+
+ auto invoke(Context const &ctx, int argc, char **argv) -> int
+ {
+ return std::invoke(_func, ctx, argc, argv);
+ }
+
+private:
+ std::function<int (Context const &, int, char **)> _func;
+};
+
+// A node in the command hierarchy.
+template<typename Context>
+struct node {
+ // The command name of this node.
+ std::string name;
+
+ // Handler for this node. May be null, which means this node has
+ // sub-commands but isn't a command itself.
+ command<Context> *handler = nullptr;
+
+ node(std::string name_)
+ : name(std::move(name_))
+ {}
+
+ // Run the handler for this node.
+ auto invoke(Context const &ctx, int argc, char **argv) const -> int
+ {
+ // Look for a subcommand with argv[0].
+ auto it = commands.find(argv[0]);
+ if (it == commands.end())
+ throw usage_error("unknown command");
+
+ auto const &child = it->second;
+
+ // If the child has a handler, invoke it.
+ if (child.handler != nullptr)
+ return child.handler->invoke(ctx, argc, argv);
+
+ --argc;
+ ++argv;
+
+ if (argc == 0)
+ throw usage_error("incomplete command");
+
+ // Otherwise, continue recursing.
+ return child.invoke(ctx, argc, argv);
+ }
+
+
+ // Create a new node under this one, or return it if it already exists.
+ // If path is empty, return this node.
+ auto create_node(std::string_view path) -> node&
+ {
+ auto child = next_word(&path);
+ if (child.empty())
+ return *this;
+
+ // If the child node doesn't exist, insert an empty node.
+ auto it = commands.find(child);
+ if (it == commands.end()) {
+ std::tie(it, std::ignore) =
+ commands.insert(std::pair{child,
+ node(std::string(child))});
+ }
+
+ if (path.empty())
+ return it->second;
+
+ return it->second.create_node(path);
+ }
+
+
+ void print_usage(std::string prefix) const {
+ if (handler != nullptr)
+ std::print("{}{}\n", prefix, name);
+
+ for (auto const &it : commands)
+ it.second.print_usage(prefix + name + " ");
+ }
+
+private:
+ std::map<std::string_view, node> commands;
+};
+
+// This may be called before main(), so catch any exceptions.
+template<typename Context>
+auto get_root_node() noexcept -> node<Context> & try {
+ static auto root_node = node<Context>("");
+ return root_node;
+} catch (std::exception const &exc) {
+ std::cerr << "ERROR: get_root_node: " << exc.what() << "\n";
+ std::exit(1);
+}
+
+export template<typename Context>
+auto dispatch_command(Context const &ctx, int argc, char **argv) -> int
+{
+ return get_root_node<Context>().invoke(ctx, argc, argv);
+}
+
+export template<typename Context>
+void print_usage(std::string_view prefix)
+{
+ get_root_node<Context>().print_usage(std::string(prefix));
+}
+
+} // namespace nihil
diff --git a/nihil/next_word.ccm b/nihil/next_word.ccm
new file mode 100644
index 0000000..b6345bc
--- /dev/null
+++ b/nihil/next_word.ccm
@@ -0,0 +1,50 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <algorithm>
+#include <locale>
+#include <ranges>
+#include <string>
+#include <utility>
+
+export module nihil:next_word;
+
+import :ctype;
+import :skipws;
+
+namespace nihil {
+
+/*
+ * Return the next word from a string_view. Skips leading whitespace, so
+ * calling this repeatedly will return each word from the string.
+ */
+
+export template<typename Char>
+auto next_word(std::basic_string_view<Char> text,
+ std::locale const &locale = std::locale())
+ -> std::pair<std::basic_string_view<Char>,
+ std::basic_string_view<Char>>
+{
+ text = skipws(text, locale);
+
+ auto is_space = ctype_is(std::ctype_base::space, locale);
+ auto split_pos = std::ranges::find_if(text, is_space);
+
+ return {{std::ranges::begin(text), split_pos},
+ {split_pos, std::ranges::end(text)}};
+}
+
+export template<typename Char>
+auto next_word(std::basic_string_view<Char> *text,
+ std::locale const &locale = std::locale())
+ -> std::basic_string_view<Char>
+{
+ auto [word, rest] = next_word(*text, locale);
+ *text = rest;
+ return word;
+}
+
+} // namespace nihil
diff --git a/nihil/nihil.ccm b/nihil/nihil.ccm
index 69cc282..0daf931 100644
--- a/nihil/nihil.ccm
+++ b/nihil/nihil.ccm
@@ -6,10 +6,14 @@ module;
export module nihil;
+export import :command_map;
export import :ctype;
export import :generator;
export import :generic_error;
export import :getenv;
export import :guard;
export import :fd;
+export import :next_word;
+export import :skipws;
export import :tabulate;
+export import :usage_error;
diff --git a/nihil/skipws.ccm b/nihil/skipws.ccm
new file mode 100644
index 0000000..20f6aa4
--- /dev/null
+++ b/nihil/skipws.ccm
@@ -0,0 +1,40 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <algorithm>
+#include <locale>
+#include <ranges>
+#include <string>
+
+export module nihil:skipws;
+
+import :ctype;
+
+namespace nihil {
+
+/*
+ * Remove leading whitespace from a string.
+ */
+
+export template<typename Char>
+auto skipws(std::basic_string_view<Char> text,
+ std::locale const &locale = std::locale())
+ -> std::basic_string_view<Char>
+{
+ auto is_space = ctype_is(std::ctype_base::space, locale);
+ auto nonws = std::ranges::find_if_not(text, is_space);
+ return {nonws, std::ranges::end(text)};
+}
+
+export template<typename Char>
+auto skipws(std::basic_string_view<Char> *text,
+ std::locale const &locale = std::locale())
+ -> void
+{
+ *text = skipws(*text, locale);
+}
+
+} // namespace nihil
diff --git a/nihil/usage_error.ccm b/nihil/usage_error.ccm
new file mode 100644
index 0000000..9b56991
--- /dev/null
+++ b/nihil/usage_error.ccm
@@ -0,0 +1,28 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <format>
+#include <utility>
+
+export module nihil:usage_error;
+
+import :generic_error;
+
+namespace nihil {
+
+/*
+ * Exception thrown to indicate invalid command-line arguments.
+ */
+export struct usage_error : generic_error {
+ template<typename... Args>
+ usage_error(std::format_string<Args...> fmt, Args &&...args)
+ : generic_error(fmt, std::forward<Args>(args)...)
+ {}
+};
+
+} // namespace nihil
+
+
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index c6788a3..13974aa 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,12 +1,15 @@
# This source code is released into the public domain.
add_executable(nihil.test
+ command_map.cc
ctype.cc
fd.cc
generator.cc
generic_error.cc
getenv.cc
guard.cc
+ next_word.cc
+ skipws.cc
tabulate.cc)
target_link_libraries(nihil.test PRIVATE
diff --git a/tests/command_map.cc b/tests/command_map.cc
new file mode 100644
index 0000000..75b6f0d
--- /dev/null
+++ b/tests/command_map.cc
@@ -0,0 +1,41 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <vector>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+namespace {
+
+auto cmd_sub1_called = false;
+auto cmd_sub1 = nihil::command<int>("cmd sub1", [](int, 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());
+ nihil::dispatch_command(0, args.size(), argv);
+ REQUIRE(cmd_sub1_called == true);
+}
+
+TEST_CASE("command_map: unknown command", "[command_map]")
+{
+ auto args = std::vector<char const *>{
+ "cmd", "nonesuch", nullptr
+ };
+ auto argv = const_cast<char **>(args.data());
+
+ REQUIRE_THROWS_AS(nihil::dispatch_command(0, args.size(), argv),
+ nihil::usage_error);
+}
diff --git a/tests/next_word.cc b/tests/next_word.cc
new file mode 100644
index 0000000..4055485
--- /dev/null
+++ b/tests/next_word.cc
@@ -0,0 +1,65 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <locale>
+#include <string>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+TEST_CASE("next_word: basic", "[next_word]")
+{
+ using namespace std::literals;
+ auto s = "foo bar baz"sv;
+
+ auto words = nihil::next_word(s);
+ REQUIRE(words.first == "foo");
+ REQUIRE(words.second == " bar baz");
+
+ auto word = nihil::next_word(&s);
+ REQUIRE(word == "foo");
+ REQUIRE(s == " bar baz");
+}
+
+TEST_CASE("next_word: multiple spaces", "[next_word]")
+{
+ using namespace std::literals;
+ auto s = "foo bar baz"sv;
+
+ auto words = nihil::next_word(s);
+ REQUIRE(words.first == "foo");
+ REQUIRE(words.second == " bar baz");
+
+ auto word = nihil::next_word(&s);
+ REQUIRE(word == "foo");
+ REQUIRE(s == " bar baz");
+}
+
+TEST_CASE("next_word: leading spaces", "[next_word]")
+{
+ using namespace std::literals;
+ auto s = " \tfoo bar baz"sv;
+
+ auto words = nihil::next_word(s);
+ REQUIRE(words.first == "foo");
+ REQUIRE(words.second == " bar baz");
+
+ auto word = nihil::next_word(&s);
+ REQUIRE(word == "foo");
+ REQUIRE(s == " bar baz");
+}
+
+TEST_CASE("next_word: locale", "[next_word]")
+{
+ using namespace std::literals;
+ auto s = L"\u2003foo\u2003bar\u2003baz"sv;
+
+ auto words = nihil::next_word(s);
+ REQUIRE(words.first == s);
+
+ words = nihil::next_word(s, std::locale("C.UTF-8"));
+ REQUIRE(words.first == L"foo");
+ REQUIRE(words.second == L"\u2003bar\u2003baz");
+}
diff --git a/tests/skipws.cc b/tests/skipws.cc
new file mode 100644
index 0000000..2159e2e
--- /dev/null
+++ b/tests/skipws.cc
@@ -0,0 +1,45 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <locale>
+#include <string>
+using namespace std::literals;
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+TEST_CASE("skipws: basic", "[skipws]")
+{
+ REQUIRE(nihil::skipws("foo"sv) == "foo");
+ REQUIRE(nihil::skipws(" foo"sv) == "foo");
+ REQUIRE(nihil::skipws("foo "sv) == "foo ");
+ REQUIRE(nihil::skipws("foo bar"sv) == "foo bar");
+}
+
+TEST_CASE("skipws: pointer", "[skipws]")
+{
+ auto s = "foo"sv;
+ nihil::skipws(&s);
+ REQUIRE(s == "foo");
+
+ s = " foo"sv;
+ nihil::skipws(&s);
+ REQUIRE(s == "foo");
+
+ s = "foo "sv;
+ nihil::skipws(&s);
+ REQUIRE(s == "foo ");
+
+ s = "foo bar"sv;
+ nihil::skipws(&s);
+ REQUIRE(s == "foo bar");
+}
+
+TEST_CASE("skipws: locale", "[skipws]")
+{
+ // Assume the default locale is C.
+ REQUIRE(nihil::skipws(L"\u2003foo"sv) == L"\u2003foo");
+ REQUIRE(nihil::skipws(L"\u2003foo"sv, std::locale("C.UTF-8")) == L"foo");
+}