diff options
| -rw-r--r-- | nihil/CMakeLists.txt | 6 | ||||
| -rw-r--r-- | nihil/command_map.ccm | 153 | ||||
| -rw-r--r-- | nihil/next_word.ccm | 50 | ||||
| -rw-r--r-- | nihil/nihil.ccm | 4 | ||||
| -rw-r--r-- | nihil/skipws.ccm | 40 | ||||
| -rw-r--r-- | nihil/usage_error.ccm | 28 | ||||
| -rw-r--r-- | tests/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | tests/command_map.cc | 41 | ||||
| -rw-r--r-- | tests/next_word.cc | 65 | ||||
| -rw-r--r-- | tests/skipws.cc | 45 |
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"); +} |
