Skip to content

Commit e7d02d5

Browse files
authored
Parse command line args (#61)
* Add CliOpts - Support --help and --version by default. - Currently no custom options / parser in use. * Comments, cleanup
1 parent 4b4f2e9 commit e7d02d5

File tree

4 files changed

+317
-88
lines changed

4 files changed

+317
-88
lines changed

lib/util/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ target_sources(${PROJECT_NAME} PRIVATE
6666
include/${target_prefix}/util/async_queue.hpp
6767
include/${target_prefix}/util/bool.hpp
6868
include/${target_prefix}/util/byte_buffer.hpp
69+
include/${target_prefix}/util/cli_opts.hpp
6970
include/${target_prefix}/util/colour_space.hpp
7071
include/${target_prefix}/util/data_provider.hpp
7172
include/${target_prefix}/util/enum_array.hpp
@@ -89,6 +90,7 @@ target_sources(${PROJECT_NAME} PRIVATE
8990
include/${target_prefix}/util/version.hpp
9091
include/${target_prefix}/util/visitor.hpp
9192

93+
src/cli_opts.cpp
9294
src/data_provider.cpp
9395
src/env.cpp
9496
src/image.cpp
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#pragma once
2+
#include <facade/util/ptr.hpp>
3+
#include <string>
4+
#include <vector>
5+
6+
namespace facade {
7+
///
8+
/// \brief Command line options parser.
9+
///
10+
struct CliOpts {
11+
///
12+
/// \brief Result of parsing options.
13+
///
14+
enum class Result { eContinue, eExitFailure, eExitSuccess };
15+
16+
///
17+
/// \brief Specification of an option key.
18+
///
19+
struct Key {
20+
///
21+
/// \brief Long form of the key.
22+
///
23+
std::string_view full{};
24+
///
25+
/// \brief Short / character form of the key.
26+
///
27+
char single{};
28+
29+
///
30+
/// \brief Check if Key is valid.
31+
/// \returns true if either single is non-null or full is non-empty
32+
///
33+
constexpr bool valid() const { return !full.empty() || single != '\0'; }
34+
};
35+
36+
///
37+
/// \brief Value for an option.
38+
///
39+
using Value = std::string_view;
40+
41+
///
42+
/// \brief Specification for an option consumed by a custom parser.
43+
///
44+
struct Opt {
45+
///
46+
/// \brief Key specification.
47+
///
48+
Key key{};
49+
///
50+
/// \brief Value specification (printed in help).
51+
///
52+
Value value{};
53+
///
54+
/// \brief If value is non-empty, whether it is optional.
55+
///
56+
bool is_optional_value{};
57+
///
58+
/// \brief Help text for this option.
59+
///
60+
std::string_view help{};
61+
};
62+
63+
///
64+
/// \brief Interface for custom parser.
65+
///
66+
struct Parser {
67+
///
68+
/// \brief Callback for a parsed option.
69+
/// \param key Option key
70+
/// \param value Value passed (if any)
71+
///
72+
virtual void opt(Key key, Value value) = 0;
73+
};
74+
75+
///
76+
/// \brief Specification for application.
77+
///
78+
struct Spec {
79+
///
80+
/// \brief List of desired options (requires a custom parser).
81+
///
82+
/// Invalid keys are removed from the list.
83+
///
84+
std::vector<Opt> options{};
85+
///
86+
/// \brief Application version (printed in help).
87+
///
88+
std::string_view version{"(unknown)"};
89+
};
90+
91+
///
92+
/// \brief Parse help, version, and custom command line args.
93+
/// \param spec Application spec
94+
/// \param out A custom parser (used if custom options are in spec)
95+
/// \param argc Number of command line arguments
96+
/// \param argv Pointer to first argument
97+
///
98+
static Result parse(Spec spec, Ptr<Parser> out, int argc, char const* const* argv);
99+
///
100+
/// \brief Parse help and version command line args.
101+
/// \param version Application version
102+
/// \param argc Number of command line arguments
103+
/// \param argv Pointer to first argument
104+
///
105+
static Result parse(std::string_view version, int argc, char const* const* argv);
106+
};
107+
} // namespace facade

lib/util/src/cli_opts.cpp

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
#include <fmt/format.h>
2+
#include <facade/util/cli_opts.hpp>
3+
#include <facade/util/visitor.hpp>
4+
#include <algorithm>
5+
#include <cassert>
6+
#include <filesystem>
7+
#include <iostream>
8+
#include <span>
9+
#include <variant>
10+
11+
namespace facade {
12+
namespace {
13+
namespace fs = std::filesystem;
14+
15+
CliOpts::Opt const* find_opt(CliOpts::Spec const& spec, std::variant<std::string_view, char> key) {
16+
for (auto const& opt : spec.options) {
17+
bool match{};
18+
auto const visitor = Visitor{
19+
[opt, &match](std::string_view const full) {
20+
if (opt.key.full == full) { match = true; }
21+
},
22+
[opt, &match](char const single) {
23+
if (opt.key.single == single) { match = true; }
24+
},
25+
};
26+
std::visit(visitor, key);
27+
if (match) { return &opt; }
28+
}
29+
return {};
30+
}
31+
32+
struct OptParser {
33+
using Result = CliOpts::Result;
34+
35+
CliOpts::Spec valid_spec{};
36+
Ptr<CliOpts::Parser> out;
37+
std::string exe_name{};
38+
39+
OptParser(CliOpts::Spec spec, Ptr<CliOpts::Parser> out, char const* arg0) : out(out) {
40+
valid_spec.version = spec.version.empty() ? "(unknown)" : spec.version;
41+
std::erase_if(spec.options, [](CliOpts::Opt const& o) { return !o.key.valid(); });
42+
valid_spec.options.reserve(spec.options.size() + 2);
43+
std::move(spec.options.begin(), spec.options.end(), std::back_inserter(valid_spec.options));
44+
valid_spec.options.push_back(CliOpts::Opt{.key = {.full = "help"}, .help = "Show this help text"});
45+
valid_spec.options.push_back(CliOpts::Opt{.key = {.full = "version"}, .help = "Display the version"});
46+
exe_name = fs::path{arg0}.filename().stem().generic_string();
47+
}
48+
49+
std::size_t get_max_width() const {
50+
auto ret = std::size_t{};
51+
for (auto const& opt : valid_spec.options) {
52+
auto width = opt.key.full.size();
53+
if (!opt.value.empty()) {
54+
width += 1; // =
55+
if (opt.is_optional_value) {
56+
width += 2; // []
57+
}
58+
width += opt.value.size();
59+
}
60+
ret = std::max(ret, width);
61+
}
62+
return ret;
63+
}
64+
65+
Result print_help() const {
66+
auto str = std::string{};
67+
str.reserve(1024);
68+
fmt::format_to(std::back_inserter(str), "Usage: {} [OPTION]...\n\n", exe_name);
69+
auto const max_width = get_max_width();
70+
for (auto const& opt : valid_spec.options) {
71+
fmt::format_to(std::back_inserter(str), " {}{}", (opt.key.single ? '-' : ' '), (opt.key.single ? opt.key.single : ' '));
72+
if (!opt.key.full.empty()) { fmt::format_to(std::back_inserter(str), "{} --{}", (opt.key.single ? ',' : ' '), opt.key.full); }
73+
auto width = opt.key.full.size();
74+
if (!opt.value.empty()) {
75+
fmt::format_to(std::back_inserter(str), "{}={}{}", (opt.is_optional_value ? "[" : ""), opt.value, (opt.is_optional_value ? "]" : ""));
76+
width += (opt.is_optional_value ? 3 : 1) + opt.value.size();
77+
}
78+
assert(width <= max_width);
79+
auto const remain = max_width - width + 4;
80+
for (std::size_t i = 0; i < remain; ++i) { str += ' '; }
81+
fmt::format_to(std::back_inserter(str), "{}\n", opt.help);
82+
}
83+
std::cout << str << '\n';
84+
return Result::eExitSuccess;
85+
}
86+
87+
Result print_version() const {
88+
std::cout << fmt::format("{} version {}\n", exe_name, valid_spec.version);
89+
return Result::eExitSuccess;
90+
}
91+
92+
CliOpts::Result opt(CliOpts::Key key, CliOpts::Value value) const {
93+
if (key.full == "help") { return print_help(); }
94+
if (key.full == "version") { return print_version(); }
95+
if (out) { out->opt(key, value); }
96+
return Result::eContinue;
97+
}
98+
};
99+
100+
struct ParseOpt {
101+
using Result = CliOpts::Result;
102+
103+
OptParser const& out;
104+
105+
std::string_view current{};
106+
107+
bool at_end() const { return current.empty(); }
108+
109+
char advance() {
110+
if (at_end()) { return {}; }
111+
auto ret = current[0];
112+
current = current.substr(1);
113+
return ret;
114+
}
115+
116+
char peek() const { return current[0]; }
117+
118+
Result unknown_option(std::string_view opt) const {
119+
std::cerr << fmt::format("unknown option: {}\n", opt);
120+
return Result::eExitFailure;
121+
}
122+
123+
Result missing_value(CliOpts::Opt const& opt) const {
124+
auto str = opt.key.full;
125+
if (str.empty()) { str = {&opt.key.single, 1}; }
126+
std::cerr << fmt::format("missing required value for option: {}{}\n", (opt.key.full.empty() ? "-" : "--"), str);
127+
return Result::eExitFailure;
128+
}
129+
130+
Result parse_word() {
131+
auto const it = current.find('=');
132+
CliOpts::Opt const* opt{};
133+
auto value = CliOpts::Value{};
134+
if (it != std::string_view::npos) {
135+
auto const key = current.substr(0, it);
136+
opt = find_opt(out.valid_spec, key);
137+
if (!opt) { return unknown_option(key); }
138+
value = current.substr(it + 1);
139+
} else {
140+
opt = find_opt(out.valid_spec, current);
141+
if (!opt) { return unknown_option(current); }
142+
}
143+
if (!opt->value.empty() && !opt->is_optional_value && value.empty()) { return missing_value(*opt); }
144+
return out.opt(opt->key, value);
145+
}
146+
147+
Result parse_singles() {
148+
char prev{};
149+
while (!at_end() && peek() != '=') {
150+
prev = advance();
151+
auto const* opt = find_opt(out.valid_spec, prev);
152+
if (!opt) { return unknown_option({&prev, 1}); }
153+
return out.opt(opt->key, {});
154+
}
155+
if (peek() == '=') {
156+
if (prev == '\0') { return unknown_option(""); }
157+
advance();
158+
auto const* opt = find_opt(out.valid_spec, prev);
159+
if (!opt) { return unknown_option({&prev, 1}); }
160+
return out.opt(opt->key, current);
161+
}
162+
return Result::eContinue;
163+
}
164+
165+
Result parse() {
166+
auto const c = advance();
167+
if (c == '-') {
168+
advance();
169+
return parse_word();
170+
}
171+
return parse_singles();
172+
}
173+
174+
Result operator()(std::string_view opt_arg) {
175+
current = opt_arg;
176+
return parse();
177+
}
178+
};
179+
180+
} // namespace
181+
182+
auto CliOpts::parse(Spec spec, Ptr<Parser> out, int argc, char const* const* argv) -> Result {
183+
if (argc < 1) { return Result::eContinue; }
184+
auto opt_parser = OptParser{std::move(spec), out, argv[0]};
185+
auto parse_opt = ParseOpt{opt_parser};
186+
for (int i = 1; i < argc; ++i) {
187+
if (auto const result = parse_opt(argv[i]); result != Result::eContinue) { return result; }
188+
}
189+
return Result::eContinue;
190+
}
191+
192+
auto CliOpts::parse(std::string_view version, int argc, char const* const* argv) -> Result {
193+
auto spec = Spec{.version = version};
194+
return parse(std::move(spec), {}, argc, argv);
195+
}
196+
} // namespace facade

0 commit comments

Comments
 (0)