blob: 5d79ec1b4def676e2365ee895b01fe642052a40b [file] [log] [blame]
/*
* Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#ifndef _CLI_H
#define _CLI_H
#include <algorithm>
#include <exception>
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <vector>
#include <cassert>
#include <iostream>
#include <numeric>
#include <sstream>
#include <limits>
/**
* Note this is a hastily hacked together command line parser.
*
* I like the syntax of clipp; but it seemed really buggy, and writing something less functional
* with similar syntax seemed like the quickest way to go.
*
* Ironically this is probably just as buggy off the happy path as clipp appeared to be, but
* in this case the happy path is ours!
*/
namespace cli {
typedef std::string string;
template<typename T> using vector = std::vector<T>;
template<typename A, typename B> using map = std::map<A, B>;
template<typename A, typename B> using pair = std::pair<A, B>;
template<typename T> using shared_ptr = std::shared_ptr<T>;
auto join = [](const vector<string> &range, const string &separator) {
if (range.empty()) return string();
return accumulate(
next(begin(range)), // there is at least 1 element, so OK.
end(range),
range[0], // the initial value
[&separator](auto result, const auto &value) {
return result + separator + value;
});
};
struct parse_error : public std::exception {
explicit parse_error(string what) : _what(std::move(what)) {}
const char *what() const noexcept override {
return _what.c_str();
}
private:
string _what;
};
struct group;
template<typename T>
struct matchable_derived;
template <typename K, typename V> struct map_and_order {
map<K,V> _map;
vector<K> _order;
V& operator[](const K& key) {
auto i = _map.find(key);
if (i == _map.end()) {
_order.push_back(key);
}
return _map[key];
}
vector<K> ordered_keys() {
return _order;
}
};
struct option_map {
typedef map_and_order<string, map_and_order<string, vector<pair<string, string>>>> container;
void add(const string& major_group, const string& minor_group, const string& option, const string& description) {
auto &v = contents[major_group][minor_group];
// we don't want to repeated the same option
if (std::find_if(v.begin(), v.end(), [&](const auto &x) { return x.first == option; }) == v.end()) {
v.emplace_back(option, description);
}
}
container contents;
};
struct matchable;
enum struct match_type {
not_yet,
match,
error,
no_match,
};
struct opaque_settings {
virtual shared_ptr<opaque_settings> copy() = 0;
virtual void save_into() = 0;
virtual void apply_from() = 0;
};
struct settings_holder {
explicit settings_holder(shared_ptr<opaque_settings> settings) : settings(settings) {}
settings_holder(const settings_holder &other) {
settings = other.settings->copy();
}
void save_into() {
settings->save_into();
}
void apply_from() {
settings->apply_from();
}
shared_ptr<opaque_settings> settings;
};
struct match_state {
vector<string> remaining_args;
string error_message;
int match_count = 0;
int error_count = 0;
// if we are an error for something mising; we should rather report on an up next
// unsupported option than our error message
bool prefer_unknown_option_message = false;
std::map<const matchable *, int> matchable_counts;
settings_holder settings;
match_state(const settings_holder& settings) : settings(settings) {}
void apply_settings_from() {
settings.apply_from();
}
void save_settings_into() {
settings.save_into();
}
match_type match_value(const matchable *matchable, std::function<bool(const string&)> filter);
match_type check_min_max(const matchable *matchable);
int get_match_count(const std::shared_ptr<matchable>& element) {
return matchable_counts[element.get()];
}
match_type update_stats(match_type type, const matchable *matchable) {
assert(type != match_type::not_yet);
if (type == match_type::match) {
match_count++;
matchable_counts[matchable]++;
} else if (type == match_type::error) {
error_count++;
matchable_counts[matchable]++;
}
return type;
}
match_type match_if_equal(const matchable *matchable, const string& s);
};
struct matcher {
};
struct matchable {
matchable() = default;
explicit matchable(string name) : _name(std::move(name)) {}
std::function<string(string)> action = [](const string&) { return ""; };
std::function<string()> missing;
virtual match_type match(match_state& m) const { return match_type::no_match; }
string name() const {
return _name;
}
virtual std::vector<string> synopsys() const {
return {_name};
}
virtual bool is_optional() const {
return !_min;
}
bool doc_non_optional() const {
return _doc_non_optional;
}
bool force_expand_help() const {
return _force_expand_help;
}
string doc() const {
return _doc;
}
virtual bool get_option_help(string major_group, string minor_group, option_map &options) const {
return false;
}
int min() const {
return _min;
}
int max() const {
return _max;
}
protected:
string _name;
string _doc;
int _min = 1;
int _max = 1;
bool _doc_non_optional = false;
bool _force_expand_help = false;
};
template<typename D>
struct matchable_derived : public matchable {
matchable_derived() = default;
explicit matchable_derived(string name) : matchable(std::move(name)) {}
D &on_action(std::function<string(const string&)> action) {
this->action = action;
return *static_cast<D *>(this);
}
D &if_missing(std::function<string()> missing) {
this->missing = missing;
return *static_cast<D *>(this);
}
D &operator%(const string& doc) {
_doc = doc;
return *static_cast<D *>(this);
}
D &required() {
_min = 1;
_max = std::max(_min, _max);
return *static_cast<D *>(this);
}
D &repeatable() {
_max = std::numeric_limits<int>::max();
return *static_cast<D *>(this);
}
D &min(int v) {
_min = v;
return *static_cast<D *>(this);
}
D &doc_non_optional(bool v) {
_doc_non_optional = v;
return *static_cast<D *>(this);
}
D &force_expand_help(bool v) {
_force_expand_help = v;
return *static_cast<D *>(this);
}
D &max(int v) {
_max = v;
return *static_cast<D *>(this);
}
std::shared_ptr<matchable> to_ptr() const {
return std::shared_ptr<matchable>(new D(*static_cast<const D *>(this)));
}
template<typename T>
group operator&(const matchable_derived<T> &m);
template<typename T>
group operator|(const matchable_derived<T> &m);
template<typename T>
group operator+(const matchable_derived<T> &m);
};
template<typename D>
struct value_base : public matchable_derived<D> {
std::function<bool(const string&)> exclusion_filter = [](const string &x){return false;};
explicit value_base(string name) : matchable_derived<D>(std::move(name)) {
this->_min = 1;
this->_max = 1;
}
vector<string> synopsys() const override {
string s = string("<") + this->_name + ">";
return {s};
}
bool get_option_help(string major_group, string minor_group, option_map &options) const override {
if (this->doc().empty()) {
return false;
}
options.add(major_group, minor_group, string("<") + this->_name + ">", this->doc());
return true;
}
match_type match(match_state& ms) const override {
match_type rc = ms.check_min_max(this);
if (rc == match_type::not_yet) {
rc = ms.match_value(this, exclusion_filter);
}
return rc;
}
D &with_exclusion_filter(std::function<bool(const string&)> exclusion_filter) {
this->exclusion_filter = exclusion_filter;
return *static_cast<D *>(this);
}
};
struct option : public matchable_derived<option> {
explicit option(char short_opt) : option(short_opt, "") {}
explicit option(string _long_opt) : option(0, std::move(_long_opt)) {}
option(char _short_opt, string _long_opt) {
_min = 0;
short_opt = _short_opt ? "-" + string(1, _short_opt) : "";
long_opt = std::move(_long_opt);
_name = short_opt.empty() ? long_opt : short_opt;
}
bool get_option_help(string major_group, string minor_group, option_map &options) const override {
if (doc().empty()) return false;
string label = short_opt.empty() ? "" : _name;
if (!long_opt.empty()) {
if (!label.empty()) label += ", ";
label += long_opt;
}
options.add(major_group, minor_group, label, doc());
return true;
}
template<typename T>
option &set(T &t) {
// note we cannot capture "this"
on_action([&t](const string& value) {
t = true;
return "";
});
return *this;
}
template<typename T>
option &clear(T &t) {
// note we cannot capture "this"
on_action([&t](const string& value) {
t = false;
return "";
});
return *this;
}
match_type match(match_state &ms) const override {
match_type rc = ms.match_if_equal(this, short_opt);
if (rc == match_type::no_match) {
rc = ms.match_if_equal(this, long_opt);
}
return rc;
}
private:
string short_opt;
string long_opt;
};
struct value : public value_base<value> {
explicit value(string name) : value_base(std::move(name)) {}
template<typename T>
value &set(T &t) {
on_action([&](const string& value) {
t = value;
return "";
});
return *this;
}
};
struct integer : public value_base<integer> {
explicit integer(string name) : value_base(std::move(name)) {}
template<typename T>
integer &set(T &t) {
int min = _min_value;
int max = _max_value;
string nm = "<" + name() + ">";
// note we cannot capture "this"
on_action([&t, min, max, nm](const string& value) {
size_t pos = 0;
long lvalue = std::numeric_limits<long>::max();
try {
lvalue = std::stol(value, &pos);
if (pos != value.length()) {
return "Garbage after integer value: " + value.substr(pos);
}
} catch (std::invalid_argument&) {
return value + " is not a valid integer";
} catch (std::out_of_range&) {
}
if (lvalue != (int)lvalue) {
return value + " is too big";
}
t = (int)lvalue;
if (t < min) {
return nm + " must be >= " + std::to_string(min);
}
if (t > max) {
return nm + " must be <= " + std::to_string(max);
}
return string("");
});
return *this;
}
integer& min_value(int v) {
_min_value = v;
return *this;
}
integer& max_value(int v) {
_max_value = v;
return *this;
}
int _min_value = 0;
int _max_value = std::numeric_limits<int>::max();
};
struct hex : public value_base<hex> {
explicit hex(string name) : value_base(std::move(name)) {}
template<typename T>
hex &set(T &t) {
unsigned int min = _min_value;
unsigned int max = _max_value;
string nm = "<" + name() + ">";
// note we cannot capture "this"
on_action([&t, min, max, nm](string value) {
auto ovalue = value;
if (value.find("0x") == 0) value = value.substr(2);
size_t pos = 0;
long lvalue = std::numeric_limits<long>::max();
try {
lvalue = std::stoul(value, &pos, 16);
if (pos != value.length()) {
return "Garbage after hex value: " + value.substr(pos);
}
} catch (std::invalid_argument&) {
return ovalue + " is not a valid hex value";
} catch (std::out_of_range&) {
}
if (lvalue != (unsigned int)lvalue) {
return value + " is not a valid 32 bit value";
}
t = (unsigned int)lvalue;
if (t < min) {
std::stringstream ss;
ss << nm << " must be >= 0x" << std::hex << std::to_string(min);
return ss.str();
}
if (t > max) {
std::stringstream ss;
ss << nm << " must be M= 0x" << std::hex << std::to_string(min);
return ss.str();
}
return string("");
});
return *this;
}
hex& min_value(unsigned int v) {
_min_value = v;
return *this;
}
hex& max_value(unsigned int v) {
_max_value = v;
return *this;
}
unsigned int _min_value = 0;
unsigned int _max_value = std::numeric_limits<unsigned int>::max();
};
struct group : public matchable_derived<group> {
enum group_type {
sequence,
set,
exclusive,
};
public:
group() : type(set) {}
template<typename T>
explicit group(const T &t) : type(set), elements{t.to_ptr()} {}
template<class Matchable, class... Matchables>
group(Matchable m, Matchable ms...) : elements{m, ms}, type(set) {}
group &set_type(group_type t) {
type = t;
return *this;
}
group &major_group(string g) {
_major_group = std::move(g);
return *this;
}
static string decorate(const matchable &e, string s) {
if (e.is_optional() && !e.doc_non_optional()) {
return string("[") + s + "]";
} else {
return s;
}
}
vector<string> synopsys() const override {
vector<string> rc;
switch (type) {
case set:
case sequence: {
std::vector<std::vector<string>> tmp{{}};
for (auto &x : elements) {
auto xs = x->synopsys();
if (xs.size() == 1) {
for (auto &s : tmp) {
s.push_back(decorate(*x, xs[0]));
}
} else {
auto save = tmp;
tmp.clear();
for (auto &v : save) {
for (auto &s : xs) {
auto nv = v;
nv.push_back(decorate(*x, s));
tmp.push_back(nv);
}
}
}
}
for (const auto &v : tmp) {
rc.push_back(join(v, " "));
}
break;
}
case exclusive:
for (auto &x : elements) {
auto xs = x->synopsys();
std::transform(xs.begin(), xs.end(), std::back_inserter(rc), [&](const auto &s) {
return decorate(*x, s);
});
}
break;
default:
assert(false);
break;
}
return rc;
}
group operator|(const group &g) {
return matchable_derived::operator|(g);
}
group operator&(const group &g) {
return matchable_derived::operator&(g);
}
group operator+(const group &g) {
return matchable_derived::operator+(g);
}
bool no_match_beats_error() const {
return _no_match_beats_error;
}
group &no_match_beats_error(bool v) {
_no_match_beats_error = v;
return *this;
}
template<typename T>
group operator&(const matchable_derived<T> &m) {
if (type == sequence) {
elements.push_back(m.to_ptr());
return *this;
}
return matchable_derived::operator&(m);
}
template<typename T>
group operator|(const matchable_derived<T> &m) {
if (type == exclusive) {
elements.push_back(m.to_ptr());
return *this;
}
return matchable_derived::operator|(m);
}
template<typename T>
group operator+(const matchable_derived<T> &m) {
if (type == set) {
elements.push_back(m.to_ptr());
return *this;
}
return matchable_derived::operator+(m);
}
bool get_option_help(string major_group, string minor_group, option_map &options) const override {
// todo beware.. this check is necessary as is, but I'm not sure what removing it breaks in terms of formatting :-(
if (is_optional() && !this->_doc_non_optional && !this->_force_expand_help) {
options.add(major_group, minor_group, synopsys()[0], doc());
return true;
}
if (!doc().empty()) {
minor_group = doc();
}
if (!_major_group.empty()) {
major_group = _major_group;
}
for (const auto &e : elements) {
e->get_option_help(major_group, minor_group, options);
}
return true;
}
match_type match(match_state& ms) const override {
match_type rc = ms.check_min_max(this);
if (rc == match_type::no_match) return rc;
assert(rc == match_type::not_yet);
switch(type) {
case sequence:
rc = match_sequence(ms);
break;
case set:
rc = match_set(ms);
break;
default:
rc = match_exclusive(ms);
break;
}
return ms.update_stats(rc, this);
}
match_type match_sequence(match_state& ms) const {
match_type rc = match_type::no_match;
for(const auto& e : elements) {
rc = e->match(ms);
assert(rc != match_type::not_yet);
if (rc != match_type::match) {
break;
}
}
return rc;
}
match_type match_set(match_state& ms) const {
// because of repeatability, we keep matching until there is nothing left to match
// vector<match_type> types(elements.size(), match_type::not_yet);
bool had_any_matches = false;
bool final_pass = false;
do {
bool matches_this_time = false;
bool errors_this_time = false;
bool not_min_this_time = false;
for (size_t i=0;i<elements.size();i++) {
// if (types[i] == match_type::not_yet) {
auto ms_prime = ms;
ms_prime.apply_settings_from();
match_type t = elements[i]->match(ms_prime);
assert(t != match_type::not_yet);
if (t == match_type::match) {
// we got a match, so record in ms and try again
// (if the matchable isn't repeatable it will no match next time)
// types[i] = match_type::not_yet;
ms_prime.save_settings_into();
ms = ms_prime;
had_any_matches = true;
matches_this_time = true;
} else if (t == match_type::error) {
if (final_pass) {
ms_prime.save_settings_into();
ms = ms_prime;
return t;
}
errors_this_time = true;
} else {
if (ms.get_match_count(elements[i]) < elements[i]->min()) {
if (final_pass) {
ms.error_message = elements[i]->missing ? elements[i]->missing() : "missing required argument";
return match_type::error;
}
not_min_this_time = true;
}
}
// }
}
if (final_pass) break;
if (!matches_this_time) {
if (errors_this_time || not_min_this_time) {
final_pass = true;
} else {
break;
}
}
} while (true);
return had_any_matches ? match_type::match : match_type::no_match;
}
match_type match_exclusive(match_state& ms) const {
vector<match_state> matches(elements.size(), ms);
vector<match_type> types(elements.size(), match_type::no_match);
int elements_with_errors = 0;
int elements_with_no_match = 0;
int error_at = -1;
int error_match_count = -1;
for (size_t i=0;i<elements.size();i++) {
match_type t;
matches[i].apply_settings_from();
do {
t = elements[i]->match(matches[i]);
assert(t != match_type::not_yet);
if (t != match_type::no_match) {
types[i] = t;
}
} while (t == match_type::match);
matches[i].save_settings_into();
if (types[i] == match_type::match) {
ms = matches[i];
return match_type::match;
} else if (types[i] == match_type::error) {
if (matches[i].match_count > error_match_count) {
error_match_count = matches[i].match_count;
error_at = i;
}
elements_with_errors++;
} else if (types[i] == match_type::no_match) {
elements_with_no_match++;
}
}
if (elements_with_no_match && (!elements_with_errors || no_match_beats_error())) {
return match_type::no_match;
}
if (elements_with_errors) {
ms = matches[error_at];
ms.apply_settings_from(); // todo perhaps want to apply the previous settings instead?
return match_type::error;
} else {
// back out any modified settings
ms.apply_settings_from();
return match_type::no_match;
}
}
private:
string _major_group;
vector<std::shared_ptr<matchable>> elements;
group_type type;
bool _no_match_beats_error = true;
};
template<typename D>
template<typename T>
group matchable_derived<D>::operator|(const matchable_derived<T> &m) {
return group{this->to_ptr(), m.to_ptr()}.set_type(group::exclusive);
}
template<typename D>
template<typename T>
group matchable_derived<D>::operator&(const matchable_derived<T> &m) {
int _min = matchable::min();
int _max = matchable::max();
min(1);
max(1);
return group{this->to_ptr(), m.to_ptr()}.set_type(group::sequence).min(_min).max(_max);
}
template<typename D>
template<typename T>
group matchable_derived<D>::operator+(const matchable_derived<T> &m) {
return group{this->to_ptr(), m.to_ptr()};
}
vector<string> make_args(int argc, char **argv) {
vector<string> args;
for (int i = 1; i < argc; i++) {
string arg(argv[i]);
if (arg.length() > 2 && arg[0] == '-' && arg[1] != '-') {
// expand collapsed args (unconditionally for now)
for (auto c = arg.begin() + 1; c != arg.end(); c++) {
args.push_back("-" + string(1, *c));
}
} else {
args.push_back(arg);
}
}
return args;
}
match_type match_state::check_min_max(const matchable *matchable) {
if (matchable_counts[matchable] < matchable->min()) {
return match_type::not_yet;
}
if (matchable_counts[matchable] >= matchable->max()) {
return match_type::no_match;
}
return match_type::not_yet;
}
match_type match_state::match_if_equal(const matchable *matchable, const string& s) {
if (remaining_args.empty()) return match_type::no_match;
if (remaining_args[0] == s) {
auto message = matchable->action(s);
assert(message.empty());
remaining_args.erase(remaining_args.begin());
return update_stats(match_type::match, matchable);
}
return match_type::no_match;
}
match_type match_state::match_value(const matchable *matchable, std::function<bool(const string&)> exclusion_filter) {
// treat an excluded value as missing
bool empty = remaining_args.empty() || exclusion_filter(remaining_args[0]);
if (empty) {
if (matchable_counts[matchable] < matchable->min()) {
prefer_unknown_option_message = !remaining_args.empty();
error_message = matchable->missing ? matchable->missing() : "missing <" + matchable->name() +">";
return update_stats(match_type::error, matchable);
}
return match_type::no_match;
}
auto message = matchable->action(remaining_args[0]);
if (!message.empty()) {
error_message = message;
return update_stats(match_type::error, matchable);
}
remaining_args.erase(remaining_args.begin());
return update_stats(match_type::match, matchable);
}
template<typename S> struct typed_settings : public opaque_settings {
explicit typed_settings(S& settings) : root_settings(settings), settings(settings) {
}
shared_ptr<cli::opaque_settings> copy() override {
auto c = std::make_shared<typed_settings<S>>(*this);
c->settings = settings;
return c;
}
void save_into() override {
settings = root_settings;
}
void apply_from() override {
root_settings = settings;
}
S& root_settings;
S settings;
};
template<typename S> void match(S& settings, const group& g, std::vector<string> args) {
auto holder = settings_holder(std::make_shared<typed_settings<S>>(settings));
match_state ms(holder);
ms.remaining_args = std::move(args);
auto t = g.match(ms);
if (!ms.prefer_unknown_option_message) {
if (t == match_type::error) {
throw parse_error(ms.error_message);
}
}
if (!ms.remaining_args.empty()) {
if (ms.remaining_args[0].find('-')==0) {
throw parse_error("unexpected option: "+ms.remaining_args[0]);
} else {
throw parse_error("unexpected argument: "+ms.remaining_args[0]);
}
}
if (ms.prefer_unknown_option_message) {
if (t == match_type::error) {
throw parse_error(ms.error_message);
}
}
}
}
#endif