FE 0.13.1
Header-only C++ frontend library
Loading...
Searching...
No Matches
cli.h
Go to the documentation of this file.
1#pragma once
2
3#include <charconv>
4#include <cstddef>
5
6#include <format>
7#include <functional>
8#include <limits>
9#include <optional>
10#include <ostream>
11#include <string>
12#include <string_view>
13#include <type_traits>
14#include <vector>
15
16/// A small command-line parser for a single command - no subcommands.
17///
18/// Declare the switches by piping fe::cli::opt / fe::cli::arg into an fe::cli::Cli and bind each one to a variable:
19/// ```
20/// bool show_help = false, verbose = false;
21/// std::string out, in;
22/// std::vector<std::string> plugins;
23///
24/// auto cli = fe::cli::Cli("mim", "The MimIR compiler.")
25/// | fe::cli::help(show_help)
26/// | fe::cli::opt(verbose )["-V"]["--verbose"]("Be verbose.")
27/// | fe::cli::opt(plugins, "plugin" )["-p"]["--plugin" ]("Loads a plugin; repeatable.")
28/// | fe::cli::group("Output")
29/// | fe::cli::opt(out, "file" )["-o"]["--output" ]("Where to write the result.")
30/// | fe::cli::arg(in, "file" ) ("Input file.");
31///
32/// if (auto err = cli.parse(argc, argv)) throw std::invalid_argument(*err);
33/// if (show_help) std::cout << cli;
34/// ```
35/// A target may be a `bool` (for a flag), a `std::string`, an integral type, a `std::vector` of those, or a callable -
36/// invoked with `true` for a flag and with the `std::string` value otherwise.
37/// Such a callable may return a `std::string` to reject that value; a non-empty one becomes the error of Cli::parse.
38/// An fe::cli::opt without a hint is a flag; one with a hint takes a value.
39///
40/// The parser understands `--name value`, `--name=value`, `-n value`, `-nvalue`, clustered short flags (`-abc`), and
41/// `--` to end option processing.
42///
43/// Cli::help lays the switches out for a terminal - wrapped to its width and colored via fe::term - whereas
44/// Cli::markdown renders the same information as Markdown tables; fe::cli::group splits both into sections.
45namespace fe::cli {
46
47// clang-format off
48class Opt;
49template<class T> Opt opt(T&);
50template<class T> Opt opt(T&, std::string);
51template<class T> Opt arg(T&, std::string);
52// clang-format on
53
54/// Starts a new section in the help output; see fe::cli::group.
55struct Group {
56 std::string name;
57};
58
59/// Opens a section named @p name that all following Cli options are listed under.
60inline Group group(std::string name) { return Group{std::move(name)}; }
61
62namespace detail {
63
64template<class T>
65inline constexpr bool always_false = false;
66
67template<class T>
68struct Elem {
69 using type = T;
70};
71
72template<class T>
73struct Elem<std::vector<T>> {
74 using type = T;
75};
76
77template<class T>
78inline constexpr bool is_vec = false;
79template<class T>
80inline constexpr bool is_vec<std::vector<T>> = true;
81
82template<class T>
83inline constexpr bool is_num = std::is_integral_v<T> && !std::is_same_v<T, bool>;
84
85/// Assigns @p s to @p t; returns a message if @p s does not scan as a `T`.
86template<class T>
87std::string scan(T& t, std::string_view s) {
88 if constexpr (std::is_same_v<T, std::string>) {
89 t = s;
90 } else if constexpr (is_vec<T>) {
91 typename Elem<T>::type elem{};
92 if (auto err = scan(elem, s); !err.empty()) return err;
93 t.emplace_back(std::move(elem));
94 } else if constexpr (is_num<T>) {
95 auto begin = s.data(), end = begin + s.size();
96 if (auto [ptr, ec] = std::from_chars(begin, end, t); ec != std::errc{} || ptr != end)
97 return std::format("'{}' is not a number", s);
98 } else if constexpr (std::is_invocable_r_v<std::string, T&, std::string>) {
99 return t(std::string(s)); // a validating handler reports what it did not like
100 } else if constexpr (std::is_invocable_v<T&, std::string>) {
101 t(std::string(s));
102 } else {
103 static_assert(always_false<T>, "cannot bind this type to an option that takes a value");
104 }
105 return {};
106}
107
108template<class T>
109std::function<std::string()> dflt(T& t) {
110 if constexpr (std::is_same_v<T, std::string>)
111 return [&t] { return t; };
112 else if constexpr (is_num<T>)
113 return [&t] { return std::format("{}", t); };
114 else
115 return {};
116}
117
118} // namespace detail
119
120/// One option or positional argument; build one with fe::cli::opt, fe::cli::arg, or fe::cli::help.
121class Opt {
122public:
123 /// Adds @p name - `"-o"` for a short, `"--output"` for a long one.
124 Opt& operator[](std::string name) { return names_.emplace_back(std::move(name)), *this; }
125
126 /// Sets the description shown in the help.
127 Opt& operator()(std::string descr) { return descr_ = std::move(descr), *this; }
128
129 /// This Opt must occur at least @p min and at most @p max times.
130 Opt& cardinality(size_t min, size_t max) { return min_ = min, max_ = max, *this; }
131
132private:
133 /// How the names are spelled out in the help - long names align under each other.
134 std::string names() const;
135 size_t width() const;
136 std::string_view label() const { return names_.empty() ? std::string_view(hint_) : names_.back(); }
137 std::string_view kind() const { return names_.empty() ? "argument" : "option"; }
138
139 std::vector<std::string> names_;
140 std::string hint_, descr_, group_;
141 std::function<std::string(std::string_view)> set_;
142 std::function<std::string()> dflt_;
143 bool value_ = false; ///< Takes a value as opposed to being a flag.
144 bool multi_ = false; ///< Bound to a `std::vector` and hence soaks up any number of values.
145 size_t min_ = 0;
146 size_t max_ = std::numeric_limits<size_t>::max();
147 size_t num_ = 0;
148
149 // clang-format off
150 friend class Cli;
151 template<class T> friend Opt opt(T&);
152 template<class T> friend Opt opt(T&, std::string);
153 template<class T> friend Opt arg(T&, std::string);
154 // clang-format on
155};
156
157/// A flag: sets @p target to `true` - or invokes it with `true` - each time it occurs.
158template<class T>
159Opt opt(T& target) {
160 Opt o;
161 o.set_ = [&target](std::string_view) -> std::string {
162 if constexpr (std::is_same_v<T, bool>)
163 target = true;
164 else
165 target(true);
166 return {};
167 };
168 return o;
169}
170
171/// An option that takes a value; @p hint names it in the help as `<hint>`.
172template<class T>
173Opt opt(T& target, std::string hint) {
174 Opt o;
175 o.hint_ = std::move(hint);
176 o.value_ = true;
177 o.multi_ = detail::is_vec<T>;
178 o.set_ = [&target](std::string_view s) { return detail::scan(target, s); };
179 o.dflt_ = detail::dflt(target);
180 return o;
181}
182
183/// A positional argument; @p hint names it in the help as `<hint>`.
184/// Bind a `std::vector` to soak up all remaining ones.
185template<class T>
186Opt arg(T& target, std::string hint) {
187 auto o = opt(target, std::move(hint));
188 o.dflt_ = {};
189 return o;
190}
191
192/// The `-h`/`--help` flag; chain `["-?"]` to give it further names.
193inline Opt help(bool& target) { return opt(target)["-h"]["--help"]("Display this help and exit."); }
194
195/// Holds the Opt%s, parses `argc`/`argv`, and renders the help.
196class Cli {
197public:
198 Cli() = default;
199 Cli(std::string prog, std::string descr = {})
200 : prog_(std::move(prog))
201 , descr_(std::move(descr)) {}
202
203 Cli& add(Opt o) {
204 if (o.group_.empty()) o.group_ = group_;
205 opts_.emplace_back(std::move(o));
206 return *this;
207 }
208
209 /// @name Add Option, Group, Section, or Epilog
210 ///@{
211 Cli& add(Group g) { return group_ = std::move(g.name), *this; }
212 Cli& operator|(Opt o) & { return add(std::move(o)); }
213 Cli& operator|(Group g) & { return add(std::move(g)); }
214 Cli&& operator|(Opt o) && { return add(std::move(o)), std::move(*this); }
215 Cli&& operator|(Group g) && { return add(std::move(g)), std::move(*this); }
216
217 /// A titled table of `term`/description rows that are not Opt%s - `ENVIRONMENT`, plugin arguments, ...
218 /// Both backends render it below the options; @p head names the first column in Cli::markdown.
219 /// Pass no @p rows to get a bare header that groups the Section%s below it - one level up in Cli::markdown.
220 Cli& section(std::string title, std::string head, std::vector<std::pair<std::string, std::string>> rows) {
221 return sections_.emplace_back(std::move(title), std::move(head), std::move(rows)), *this;
222 }
223
224 /// Text printed below the option list.
225 Cli& epilog(std::string s) { return epilog_ = std::move(s), *this; }
226 ///@}
227
228 /// Parses `argc`/`argv`; returns the error message - and nothing at all if all went well.
229 std::optional<std::string> parse(int argc, const char* const* argv);
230 void help(std::ostream&) const; ///< Renders the help for a terminal.
231 void markdown(std::ostream&) const; ///< Renders the same information as Doxygen-flavored Markdown tables.
232
233private:
234 std::string usage() const;
235
236 /// Names of the Opt groups in the order they first occur; the default group is the empty name.
237 std::vector<std::string_view> groups() const;
238
239 Opt* find(std::string_view name);
240
241 struct Section {
242 std::string title, head;
243 std::vector<std::pair<std::string, std::string>> rows;
244 };
245
246 std::string prog_, descr_, epilog_, group_;
247 std::vector<Opt> opts_;
248 std::vector<Section> sections_;
249
250 friend std::ostream& operator<<(std::ostream& os, const Cli& cli) { return cli.help(os), os; }
251};
252
253} // namespace fe::cli
Cli & add(Group g)
Definition cli.h:211
void help(std::ostream &) const
Renders the help for a terminal.
void markdown(std::ostream &) const
Renders the same information as Doxygen-flavored Markdown tables.
Cli(std::string prog, std::string descr={})
Definition cli.h:199
Cli()=default
Cli & operator|(Group g) &
Definition cli.h:213
friend std::ostream & operator<<(std::ostream &os, const Cli &cli)
Definition cli.h:250
Cli & epilog(std::string s)
Text printed below the option list.
Definition cli.h:225
std::optional< std::string > parse(int argc, const char *const *argv)
Parses argc/argv; returns the error message - and nothing at all if all went well.
Cli && operator|(Opt o) &&
Definition cli.h:214
Cli && operator|(Group g) &&
Definition cli.h:215
Cli & operator|(Opt o) &
Definition cli.h:212
Cli & add(Opt o)
Definition cli.h:203
Cli & section(std::string title, std::string head, std::vector< std::pair< std::string, std::string > > rows)
A titled table of term/description rows that are not Opts - ENVIRONMENT, plugin arguments,...
Definition cli.h:220
One option or positional argument; build one with fe::cli::opt, fe::cli::arg, or fe::cli::help.
Definition cli.h:121
friend Opt arg(T &, std::string)
A positional argument; hint names it in the help as <hint>.
Definition cli.h:186
friend class Cli
Definition cli.h:150
Opt & cardinality(size_t min, size_t max)
This Opt must occur at least min and at most max times.
Definition cli.h:130
friend Opt opt(T &)
A flag: sets target to true - or invokes it with true - each time it occurs.
Definition cli.h:159
Opt & operator()(std::string descr)
Sets the description shown in the help.
Definition cli.h:127
Opt & operator[](std::string name)
Adds name - "-o" for a short, "--output" for a long one.
Definition cli.h:124
A small command-line parser for a single command - no subcommands.
Definition cli.h:45
Group group(std::string name)
Opens a section named name that all following Cli options are listed under.
Definition cli.h:60
Opt help(bool &target)
The -h/--help flag; chain ["-?"] to give it further names.
Definition cli.h:193
Opt opt(T &)
A flag: sets target to true - or invokes it with true - each time it occurs.
Definition cli.h:159
std::string name
Definition cli.h:56
Opt arg(T &, std::string)
A positional argument; hint names it in the help as <hint>.
Definition cli.h:186
Starts a new section in the help output; see fe::cli::group.
Definition cli.h:55
Definition span.h:129