aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.cli/command.ccm
blob: 1bcb13f9f37b9ddceaeaad29a23af113b082e298 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// This source code is released into the public domain.
module;

// For EX_USAGE.  While <sysexits.h> is deprecated, there's no other standard
// exit code for 'usage error'; some programs use 2 (common on Linux), but
// 2 is also used for many other exit codes.
#include <sysexits.h>

export module nihil.cli:command;

// command_node represents a possibly-invocable command.

import nihil.std;
import nihil.core;
import :usage_error;

namespace nihil {

export struct command;

// registry.ccm
auto register_command(command *cmd) noexcept -> void;

// A command.  If constructed with a handler, this is a "real" command which can be invoked.
// Otherwise, it's a stub command that has children in the command tree.
//
// Because commands are intended to be global objects, they are not copyable or movable.
// Creating an object automatically registers it in the list of commands.
export struct command final
{
	using command_handler_t = std::function<int(int, char **)>;

	// Construct a new command with a handler and register it.  Since this is usually invoked
	// at global scope by static object construction, we handle exception internally.
	command(std::string_view const path, std::string_view const usage, auto &&handler) noexcept
	try	: m_path(path)
		, m_usage(usage)
		, m_handler(std::forward<decltype(handler)>(handler))
	{
		register_command(this);
	} catch (std::exception const &exc) {
		std::println(std::cerr, "register_command(): {}", exc.what());
		std::quick_exit(1);
	} catch (...) {
		std::println(std::cerr, "register_command(): unknown error");
		std::quick_exit(1);
	}

	// Create a stub command which doesn't have a handler.
	explicit command(std::string_view const path)
		: m_path(path)
	{
	}

	// Not copyable.
	command(command const &) = delete;
	auto operator=(command const &) -> command & = delete;

	// Not movable.
	command(command &&) = delete;
	auto operator=(command &&) -> command & = delete;

	~command() = default;

	// Return the full path for this command.
	[[nodiscard]] auto path(this command const &self) noexcept -> std::string_view
	{
		return self.m_path;
	}

	// Return the one-line usage summary for this command.
	[[nodiscard]] auto usage(this command const &self) noexcept -> std::string_view
	{
		return self.m_usage;
	}

	// Test if this command can be invoked.
	[[nodiscard]] auto invocable(this command const &self) noexcept -> bool
	{
		return static_cast<bool>(self.m_handler);
	}

	// Invoke this command and return its result.
	[[nodiscard]] auto invoke(this command const &self, int argc, char **argv)
		-> std::expected<int, error>
	{
		if (!self.m_handler)
			return error(errc::incomplete_command);

		try {
			return std::invoke(self.m_handler, argc, argv);
		} catch (usage_error const &err) {
			std::println(std::cerr, "{}", err.what());
			std::println(std::cerr, "usage: {} {}", self.path(), self.usage());
			return EX_USAGE;
		}
	}

private:
	std::string       m_path;
	std::string       m_usage;
	command_handler_t m_handler;
};

} // namespace nihil