FE 0.15.0
A C++23 toolkit for writing compiler/interpreter frontends.
Loading...
Searching...
No Matches
cli.h
Go to the documentation of this file.
1#pragma once
2
3#include <cassert>
4#include <charconv>
5#include <cstddef>
6
7#include <format>
8#include <functional>
9#include <limits>
10#include <optional>
11#include <ostream>
12#include <string>
13#include <string_view>
14#include <type_traits>
15#include <vector>
16
17namespace fe {
18
19/// A small command-line parser for a single command - no subcommands.
20///
21/// Declare the switches with Cli::opt / Cli::arg and bind each one to a variable:
22/// ```
23/// bool show_help = false, verbose = false;
24/// std::string out, in;
25/// std::vector<std::string> plugins;
26///
27/// auto cli = fe::Cli("mim", "The MimIR compiler.")
28/// .help(show_help)
29/// .opt(verbose, "-V", "--verbose", "Be verbose.")
30/// .opt(plugins, "plugin", "-p", "--plugin" , "Loads a plugin; repeatable.")
31/// .grp("Output")
32/// .opt(out , "file" , "-o", "--output" , "Where to write the result.")
33/// .arg(in , "file" , "Input file.");
34///
35/// if (auto err = cli.parse(argc, argv)) throw std::invalid_argument(*err);
36/// if (show_help) std::cout << cli;
37/// ```
38/// A `bool` target - or a callable taking one - is a flag: it is set each time it occurs; a `bool` takes no hint at
39/// all, a callable an empty one.
40/// Any other one needs a hint, listed as `<hint>` in the help, and is assigned the value: a `std::string`, an integral
41/// type, a `std::vector` of those, or a callable, which may return a `std::string` to reject the value - a non-empty
42/// one becomes the error of Cli::parse.
43///
44/// The parser understands `--name value`, `--name=value`, `-n value`, `-nvalue`, clustered short flags (`-abc`), and
45/// `--` to end option processing.
46///
47/// Cli::help lays the switches out for a terminal - wrapped to its width and colored via fe::term - whereas
48/// Cli::markdown renders the same information as Markdown tables; Cli::grp splits both into sections.
49/// A description may cite code as `` `this` ``: Cli::help colors it like CodeDiag does, Cli::markdown turns
50/// it into a code span.
51class Cli {
52public:
53 using Row = std::pair<std::string, std::string>;
54 using Rows = std::vector<Row>;
55
56 Cli() = default;
57 Cli(std::string prog, std::string descr = {})
58 : prog_(std::move(prog))
59 , descr_(std::move(descr)) {}
60
61 /// @name Declare Options and Arguments
62 ///@{
63
64 /// An option bound to @p target and named @p sname and/or @p lname - `"-o"` and `"--output"`.
65 template<class T>
66 Cli& opt(T& target, std::string hint = {}, std::string sname = {}, std::string lname = {}, std::string descr = {}) {
67 static_assert(Is_Flag<T> || std::is_same_v<T, std::string> || Vec<T>::is
68 || Is_Num<T> || std::is_invocable_v<T&, std::string>,
69 "cannot bind this type to an option");
70 auto& o = opts_.emplace_back(std::move(sname), std::move(lname), std::move(hint), std::move(descr), grp_);
71 assert(Is_Flag<T> != o.takes_value() && "a flag takes no <hint>, anything else needs one");
72 assert(!(Is_Flag<T> && o.is_arg()) && "a flag needs a name");
73 assert((o.sname.empty() || o.sname.starts_with('-')) && "a short name starts with `-`");
74 assert((o.lname.empty() || o.lname.starts_with("--")) && "a long name starts with `--`");
75 o.multi = Vec<T>::is;
76 o.dflt = dflt(target);
77 o.set = [&target](std::string_view s) { return assign(target, s); };
78 return *this;
79 }
80
81 Cli& opt(bool& target, std::string sname = {}, std::string lname = {}, std::string descr = {}) {
82 return opt<bool>(target, "", std::move(sname), std::move(lname), std::move(descr));
83 }
84
85 /// A positional argument; @p hint names it in the help as `<hint>`.
86 /// Bind a `std::vector` to soak up all remaining ones.
87 template<class T>
88 Cli& arg(T& target, std::string hint, std::string descr = {}) {
89 opt(target, std::move(hint), {}, {}, std::move(descr));
90 return opts_.back().dflt.clear(), *this;
91 }
92
93 /// The help flag - named `-h`/`--help` unless @p sname / @p lname say otherwise.
94 Cli& help(bool& target, std::string sname = "-h", std::string lname = "--help") {
95 return opt(target, std::move(sname), std::move(lname), "Display this help and exit.");
96 }
97
98 /// The Cli::opt / Cli::arg declared last must occur at least @p min and at most @p max times.
99 Cli& cardinality(size_t min, size_t max) {
100 assert(!opts_.empty() && "no option to apply a cardinality to");
101 return opts_.back().min = min, opts_.back().max = max, *this;
102 }
103
104 /// Opens a section named @p name that all following options are listed under.
105 Cli& grp(std::string name) { return grp_ = std::move(name), *this; }
106
107 /// A titled table of `term`/description rows that are not options - `ENVIRONMENT`, plugin arguments, ...
108 /// Both backends render it below the options; @p head names the first column in Cli::markdown.
109 /// Pass no @p rows to get a bare header that groups the sections below it - one level up in Cli::markdown.
110 Cli& section(std::string title, std::string head = {}, Rows rows = {}) {
111 return sections_.emplace_back(std::move(title), std::move(head), std::move(rows)), *this;
112 }
113
114 /// Text printed below the option list.
115 Cli& epilog(std::string s) { return epilog_ = std::move(s), *this; }
116 ///@}
117
118 /// Parses `argc`/`argv`; returns the error message - and nothing at all if all went well.
119 std::optional<std::string> parse(int argc, const char* const* argv);
120 void help(std::ostream&) const; ///< Renders the help for a terminal.
121 void markdown(std::ostream&) const; ///< Renders the same information as Doxygen-flavored Markdown tables.
122
123private:
124 // clang-format off
125 /// `Vec<T>::Elem` is `T`'s element type, if `T` is a `std::vector`, and `T` itself otherwise.
126 template<class T> struct Vec { using Elem = T; static constexpr bool is = false; };
127 template<class T> struct Vec<std::vector<T>> { using Elem = T; static constexpr bool is = true; };
128 // clang-format on
129
130 template<class T>
131 static constexpr bool Is_Num = std::is_integral_v<T> && !std::is_same_v<T, bool>;
132
133 /// A `bool` - or a callable taking one - is a flag; anything else is assigned the value.
134 template<class T>
135 static constexpr bool Is_Flag
136 = std::is_same_v<T, bool> || (std::is_invocable_v<T&, bool> && !std::is_invocable_v<T&, std::string>);
137
138 /// Applies @p s to @p t; returns a message if @p s does not scan as a `T`.
139 template<class T>
140 static std::string assign(T& t, std::string_view s) {
141 if constexpr (std::is_same_v<T, bool>) {
142 t = true;
143 } else if constexpr (std::is_same_v<T, std::string>) {
144 t = s;
145 } else if constexpr (Vec<T>::is) {
146 typename Vec<T>::Elem elem{};
147 if (auto err = assign(elem, s); !err.empty()) return err;
148 t.emplace_back(std::move(elem));
149 } else if constexpr (Is_Num<T>) {
150 auto begin = s.data(), end = begin + s.size();
151 if (auto [ptr, ec] = std::from_chars(begin, end, t); ec != std::errc{} || ptr != end)
152 return std::format("'{}' is not a number", s);
153 } else if constexpr (std::is_invocable_r_v<std::string, T&, std::string>) {
154 return t(std::string(s)); // a validating handler reports what it did not like
155 } else if constexpr (std::is_invocable_v<T&, std::string>) {
156 t(std::string(s));
157 } else {
158 t(true); // a flag bound to a callable
159 }
160 return {};
161 }
162
163 /// What @p t holds before parsing and hence shows as `[default: ...]` in the help.
164 template<class T>
165 static std::string dflt(const T& t) {
166 if constexpr (std::is_same_v<T, std::string>)
167 return t;
168 else if constexpr (Is_Num<T>)
169 return std::format("{}", t);
170 else
171 return {};
172 }
173
174 struct Opt {
175 std::string names(bool pad = false) const; ///< `-o, --output`; @p pad aligns a lone long name.
176 std::string spec(bool pad = false) const; ///< Cli::Opt::names plus `<hint>`, if it takes a value.
177 size_t width() const { return spec(true).size(); }
178 bool is_arg() const { return sname.empty() && lname.empty(); }
179 bool takes_value() const { return !hint.empty(); }
180 std::string_view kind() const { return is_arg() ? "argument" : "option"; }
181 std::string_view label() const { return !lname.empty() ? lname : sname.empty() ? hint : sname; }
182
183 std::string sname, lname, hint, descr, grp, dflt;
184 std::function<std::string(std::string_view)> set;
185 bool multi = false; ///< Bound to a `std::vector` and hence soaks up any number of values.
186 size_t min = 0;
187 size_t max = std::numeric_limits<size_t>::max();
188 size_t num = 0;
189 };
190
191 struct Section {
192 std::string title, head;
193 Rows rows;
194 };
195
196 std::string usage() const;
197
198 /// Names of the Opt groups in the order they first occur; the default group is the empty name.
199 std::vector<std::string_view> grps() const;
200
201 Opt* find(std::string_view name);
202
203 std::string prog_, descr_, epilog_, grp_;
204 std::vector<Opt> opts_;
205 std::vector<Section> sections_;
206
207 friend std::ostream& operator<<(std::ostream& os, const Cli& cli) { return cli.help(os), os; }
208};
209
210} // namespace fe
std::pair< std::string, std::string > Row
Definition cli.h:53
Cli & section(std::string title, std::string head={}, Rows rows={})
A titled table of term/description rows that are not options - ENVIRONMENT, plugin arguments,...
Definition cli.h:110
std::vector< Row > Rows
Definition cli.h:54
Cli & help(bool &target, std::string sname="-h", std::string lname="--help")
The help flag - named -h/--help unless sname / lname say otherwise.
Definition cli.h:94
Cli & opt(bool &target, std::string sname={}, std::string lname={}, std::string descr={})
Definition cli.h:81
Cli & arg(T &target, std::string hint, std::string descr={})
A positional argument; hint names it in the help as <hint>.
Definition cli.h:88
void help(std::ostream &) const
Renders the help for a terminal.
Cli & epilog(std::string s)
Text printed below the option list.
Definition cli.h:115
friend std::ostream & operator<<(std::ostream &os, const Cli &cli)
Definition cli.h:207
Cli & opt(T &target, std::string hint={}, std::string sname={}, std::string lname={}, std::string descr={})
An option bound to target and named sname and/or lname - "-o" and "--output".
Definition cli.h:66
Cli & cardinality(size_t min, size_t max)
The Cli::opt / Cli::arg declared last must occur at least min and at most max times.
Definition cli.h:99
void markdown(std::ostream &) const
Renders the same information as Doxygen-flavored Markdown tables.
Cli & grp(std::string name)
Opens a section named name that all following options are listed under.
Definition cli.h:105
Cli()=default
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(std::string prog, std::string descr={})
Definition cli.h:57
Definition algo.h:17
constexpr std::uint64_t pad(std::uint64_t offset, std::uint64_t align) noexcept
Rounds offset up to the next multiple of align.
Definition algo.h:42
Definition span.h:150