Commit 7c2a4f2e authored by Dominik Charousset's avatar Dominik Charousset

Add marker to make categories optional on the CLI

Categories are great at organizing program options. However, on the CLI
they get in the way quickly. This change allows developers to prefix
category names with '?' to make it optional on the CLI. For example, an
option added via

```
opt_group{custom_options_, "?foo"}
.add<std::string>("bar,b", "some string");
```

allows users to use any of these on the CLI:

- `-b text`
- `--bar=text`
- `--foo.bar=text`

All options in the `global` category already allowed this abbreviated
form implicitly.
parent 7c226fbc
...@@ -100,6 +100,9 @@ public: ...@@ -100,6 +100,9 @@ public:
/// Returns whether this config option stores a boolean flag. /// Returns whether this config option stores a boolean flag.
bool is_flag() const noexcept; bool is_flag() const noexcept;
/// Returns whether the category is optional for CLI options.
bool has_flat_cli_name() const noexcept;
/// Tries to parse an input string. Stores and returns the parsed value on /// Tries to parse an input string. Stores and returns the parsed value on
/// success, returns an error otherwise. /// success, returns an error otherwise.
expected<config_value> parse(string_view input) const; expected<config_value> parse(string_view input) const;
......
...@@ -55,7 +55,7 @@ config_option::config_option(string_view category, string_view name, ...@@ -55,7 +55,7 @@ config_option::config_option(string_view category, string_view name,
CAF_ASSERT(ts <= std::numeric_limits<uint16_t>::max()); CAF_ASSERT(ts <= std::numeric_limits<uint16_t>::max());
buf_size_ = static_cast<uint16_t>(ts); buf_size_ = static_cast<uint16_t>(ts);
buf_.reset(new char[ts]); buf_.reset(new char[ts]);
// fille the buffer with "<category>.<long-name>,<short-name>,<descriptions>" // fill the buffer with "<category>.<long-name>,<short-name>,<descriptions>"
auto first = buf_.get(); auto first = buf_.get();
auto i = first; auto i = first;
auto pos = [&] { auto pos = [&] {
...@@ -109,7 +109,7 @@ void swap(config_option& first, config_option& second) noexcept { ...@@ -109,7 +109,7 @@ void swap(config_option& first, config_option& second) noexcept {
// -- properties --------------------------------------------------------------- // -- properties ---------------------------------------------------------------
string_view config_option::category() const noexcept { string_view config_option::category() const noexcept {
return buf_slice(0, category_separator_); return buf_slice(buf_[0] == '?' ? 1 : 0, category_separator_);
} }
string_view config_option::long_name() const noexcept { string_view config_option::long_name() const noexcept {
...@@ -125,7 +125,7 @@ string_view config_option::description() const noexcept { ...@@ -125,7 +125,7 @@ string_view config_option::description() const noexcept {
} }
string_view config_option::full_name() const noexcept { string_view config_option::full_name() const noexcept {
return buf_slice(0, long_name_separator_); return buf_slice(buf_[0] == '?' ? 1 : 0, long_name_separator_);
} }
error config_option::check(const config_value& x) const { error config_option::check(const config_value& x) const {
...@@ -148,6 +148,10 @@ bool config_option::is_flag() const noexcept { ...@@ -148,6 +148,10 @@ bool config_option::is_flag() const noexcept {
return type_name() == "boolean"; return type_name() == "boolean";
} }
bool config_option::has_flat_cli_name() const noexcept {
return buf_[0] == '?' || category() == "global";
}
expected<config_value> config_option::parse(string_view input) const { expected<config_value> config_option::parse(string_view input) const {
return meta_->parse(value_, input); return meta_->parse(value_, input);
} }
......
...@@ -64,7 +64,7 @@ std::string config_option_set::help_text(bool global_only) const { ...@@ -64,7 +64,7 @@ std::string config_option_set::help_text(bool global_only) const {
string_builder sb; string_builder sb;
if (x.short_names().empty()) { if (x.short_names().empty()) {
sb << " --"; sb << " --";
if (x.category() != "global") if (!x.has_flat_cli_name())
sb << x.category() << '.'; sb << x.category() << '.';
sb << x.long_name(); sb << x.long_name();
if (!x.is_flag()) if (!x.is_flag())
...@@ -74,7 +74,7 @@ std::string config_option_set::help_text(bool global_only) const { ...@@ -74,7 +74,7 @@ std::string config_option_set::help_text(bool global_only) const {
for (auto c : x.short_names()) for (auto c : x.short_names())
sb << '-' << c << '|'; sb << '-' << c << '|';
sb << "--"; sb << "--";
if (x.category() != "global") if (!x.has_flat_cli_name())
sb << x.category() << '.'; sb << x.category() << '.';
sb << x.long_name() << ") "; sb << x.long_name() << ") ";
} }
...@@ -88,11 +88,15 @@ std::string config_option_set::help_text(bool global_only) const { ...@@ -88,11 +88,15 @@ std::string config_option_set::help_text(bool global_only) const {
std::multimap<string_view, pair> args; std::multimap<string_view, pair> args;
size_t max_arg_size = 0; size_t max_arg_size = 0;
for (auto& opt : opts_) { for (auto& opt : opts_) {
if (!global_only || opt.category() == "global") { // We treat all options with flat name as-if the category was 'global'.
if (!global_only || opt.has_flat_cli_name()) {
auto arg = build_argument(opt); auto arg = build_argument(opt);
max_arg_size = std::max(max_arg_size, arg.size()); max_arg_size = std::max(max_arg_size, arg.size());
categories.emplace(opt.category()); string_view category = "global";
args.emplace(opt.category(), std::make_pair(std::move(arg), &opt)); if (!opt.has_flat_cli_name())
category = opt.category();
categories.emplace(category);
args.emplace(category, std::make_pair(std::move(arg), &opt));
} }
} }
// Build help text by iterating over all categories in the multimap. // Build help text by iterating over all categories in the multimap.
...@@ -227,18 +231,18 @@ config_option_set::cli_long_name_lookup(string_view name) const { ...@@ -227,18 +231,18 @@ config_option_set::cli_long_name_lookup(string_view name) const {
string_view long_name; string_view long_name;
auto sep = name.find('.', offset); auto sep = name.find('.', offset);
if (sep == string::npos) { if (sep == string::npos) {
category = "global"; long_name = name.substr(offset);
if (offset == 0)
long_name = name;
else
long_name = name.substr(offset);
} else { } else {
category = name.substr(offset, sep); category = name.substr(offset, sep);
long_name = name.substr(sep + 1); long_name = name.substr(sep + 1);
} }
// Scan all options for a match. // Scan all options for a match.
auto category_match = [&](const config_option& opt) {
return sep == string::npos ? opt.has_flat_cli_name()
: opt.category() == category;
};
return detail::ptr_find_if(opts_, [&](const config_option& opt) { return detail::ptr_find_if(opts_, [&](const config_option& opt) {
return opt.category() == category && opt.long_name() == long_name; return category_match(opt) && opt.long_name() == long_name;
}); });
} }
......
...@@ -181,3 +181,12 @@ CAF_TEST(type timespan) { ...@@ -181,3 +181,12 @@ CAF_TEST(type timespan) {
timespan dur{500}; timespan dur{500};
CAF_CHECK_EQUAL(unbox(read<timespan>("500ns")), dur); CAF_CHECK_EQUAL(unbox(read<timespan>("500ns")), dur);
} }
CAF_TEST(flat CLI parsing) {
auto x = make_config_option<std::string>("?foo", "bar,b", "test option");
CAF_CHECK_EQUAL(x.category(), "foo");
CAF_CHECK_EQUAL(x.long_name(), "bar");
CAF_CHECK_EQUAL(x.short_names(), "b");
CAF_CHECK_EQUAL(x.full_name(), "foo.bar");
CAF_CHECK_EQUAL(x.has_flat_cli_name(), true);
}
...@@ -39,16 +39,18 @@ struct fixture { ...@@ -39,16 +39,18 @@ struct fixture {
config_option_set opts; config_option_set opts;
template <class T> template <class T>
expected<T> read(std::vector<std::string> args) { expected<T> read(std::vector<std::string> args) {
settings cfg; settings cfg;
auto res = opts.parse(cfg, std::move(args)); auto res = opts.parse(cfg, std::move(args));
if (res.first != pec::success) if (res.first != pec::success)
return res.first; return res.first;
auto x = get_if<T>(&cfg, "value"); auto x = get_if<T>(&cfg, key);
if (x == none) if (x == none)
return sec::invalid_argument; return sec::invalid_argument;
return *x; return *x;
} }
std::string key = "value";
}; };
} // namespace <anonymous> } // namespace <anonymous>
...@@ -140,4 +142,13 @@ CAF_TEST(string parameters) { ...@@ -140,4 +142,13 @@ CAF_TEST(string parameters) {
CAF_CHECK_EQUAL(read<std::string>({"-v123"}), "123"); CAF_CHECK_EQUAL(read<std::string>({"-v123"}), "123");
} }
CAF_TEST(flat CLI options) {
key = "foo.bar";
opts.add<std::string>("?foo", "bar,b", "some value");
CAF_CHECK(opts.begin()->has_flat_cli_name());
CAF_CHECK_EQUAL(read<std::string>({"-b", "foobar"}), "foobar");
CAF_CHECK_EQUAL(read<std::string>({"--bar=foobar"}), "foobar");
CAF_CHECK_EQUAL(read<std::string>({"--foo.bar=foobar"}), "foobar");
}
CAF_TEST_FIXTURE_SCOPE_END() CAF_TEST_FIXTURE_SCOPE_END()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment