1
0
mirror of https://github.com/CLIUtils/CLI11.git synced 2025-05-01 05:03:52 +00:00

Adding better parser

This commit is contained in:
Henry Fredrick Schreiner 2017-01-26 14:17:34 -05:00
parent 2db15139cb
commit ce30812998
3 changed files with 358 additions and 85 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
a.out*

428
CLI.hpp
View File

@ -9,12 +9,15 @@
#include <algorithm> #include <algorithm>
#include <sstream> #include <sstream>
#include <type_traits> #include <type_traits>
#include <unordered_set>
#include <iomanip>
#include <numeric>
// This is unreachable outside this file; you should not use Combiner directly // This is unreachable outside this file; you should not use Combiner directly
namespace { namespace {
void logit(std::string output) { void logit(std::string output) {
std::cout << output << std::endl; std::cout << "\033[1;31m" << output << "\033[0m" << std::endl;
} }
template <typename T> template <typename T>
@ -30,8 +33,6 @@ std::string join(const T& v, std::string delim = ",") {
} }
struct Combiner { struct Combiner {
int num; int num;
bool positional; bool positional;
@ -41,7 +42,7 @@ struct Combiner {
/// Can be or-ed together /// Can be or-ed together
Combiner operator | (Combiner b) const { Combiner operator | (Combiner b) const {
Combiner self; Combiner self;
self.num = num + b.num; self.num = std::min(num, b.num) == -1 ? -1 : std::max(num, b.num);
self.positional = positional || b.positional; self.positional = positional || b.positional;
self.required = required || b.required; self.required = required || b.required;
self.defaulted = defaulted || b.defaulted; self.defaulted = defaulted || b.defaulted;
@ -56,40 +57,66 @@ struct Combiner {
return *this | b; return *this | b;
} }
}; };
} }
namespace CLI { namespace CLI {
class BadNameString : public std::runtime_error { class Error : public std::runtime_error {
public: public:
BadNameString(std::string name) : runtime_error("Failed to parse: " + name) {}; Error(std::string name) : runtime_error(name) {};
}; };
class CallForHelp : public std::runtime_error { class BadNameString : public Error {
public: public:
CallForHelp() : runtime_error("Help option passed") {}; BadNameString(std::string name) : Error("Failed to parse: " + name) {};
}; };
class ParseError : public std::runtime_error { class CallForHelp : public Error {
public: public:
ParseError(std::string info="") : runtime_error(info) {}; CallForHelp() : Error("Help option passed") {};
}; };
class OptionAlreadyAdded : public std::runtime_error { class ParseError : public Error {
public: public:
OptionAlreadyAdded(std::string name) : runtime_error("Already added:" + name) {}; ParseError(std::string info="") : Error(info) {};
}; };
class OptionAlreadyAdded : public Error {
public:
OptionAlreadyAdded(std::string name) : Error("Already added:" + name) {};
};
class OptionNotFound : public Error {
public:
OptionNotFound(std::string name) : Error(name) {};
};
class RequiredError : public Error {
public:
RequiredError(std::string name="") : Error(name) {};
};
class ExtraPositionalsError : public Error {
public:
ExtraPositionalsError(std::string name="") : Error(name) {};
};
class HorribleError : public Error {
public:
HorribleError(std::string name="") : Error("You should never see this error! "+name) {};
};
class IncorrectConstruction : public Error {
public:
IncorrectConstruction(std::string name="") : Error(name) {};
};
const std::regex reg_split{R"regex((?:([a-zA-Z0-9]?)(?:,|$)|^)([a-zA-Z0-9][a-zA-Z0-9_\-]*)?)regex"}; const std::regex reg_split{R"regex((?:([a-zA-Z0-9]?)(?:,|$)|^)([a-zA-Z0-9][a-zA-Z0-9_\-]*)?)regex"};
const std::regex reg_short{R"regex(-([^-])(.*))regex"}; const std::regex reg_short{R"regex(-([^-])(.*))regex"};
const std::regex reg_long{R"regex(--([^-^=][^=]*)=?(.*))regex"}; const std::regex reg_long{R"regex(--([^-^=][^=]*)=?(.*))regex"};
std::tuple<std::string, std::string> split(std::string fullname) throw(BadNameString) { std::tuple<std::string, std::string> split(std::string fullname) {
std::smatch match; std::smatch match;
if (std::regex_match(fullname, match, reg_split)) { if (std::regex_match(fullname, match, reg_split)) {
@ -101,11 +128,12 @@ std::tuple<std::string, std::string> split(std::string fullname) throw(BadNameSt
} else throw BadNameString(fullname); } else throw BadNameString(fullname);
} }
const Combiner NOTHING {0,false,false,false}; const Combiner NOTHING {0, false,false,false};
const Combiner REQUIRED {0,false,true, false}; const Combiner REQUIRED {1, false,true, false};
const Combiner DEFAULT {0,false,false,true}; const Combiner DEFAULT {1, false,false,true};
const Combiner POSITIONAL{0,true, false,false}; const Combiner POSITIONAL{1, true, false,false};
const Combiner ARGS {1,false,false,false}; const Combiner ARGS {1, false,false,false};
const Combiner UNLIMITED {-1,false,false,false};
typedef std::vector<std::vector<std::string>> results_t; typedef std::vector<std::vector<std::string>> results_t;
typedef std::function<bool(results_t)> callback_t; typedef std::function<bool(results_t)> callback_t;
@ -125,11 +153,27 @@ protected:
public: public:
Option(std::string name, std::string discription = "", Combiner opts=NOTHING, std::function<bool(results_t)> callback=[](results_t){return true;}) throw (BadNameString) : Option(std::string name, std::string discription = "", Combiner opts=NOTHING, std::function<bool(results_t)> callback=[](results_t){return true;}) :
opts(opts), discription(discription), callback(callback){ opts(opts), discription(discription), callback(callback){
std::tie(sname, lname) = split(name); std::tie(sname, lname) = split(name);
} }
bool required() const {
return opts.required;
}
int expected() const {
return opts.num;
}
bool positional() const {
return opts.positional;
}
bool defaulted() const {
return opts.defaulted;
}
/// Process the callback /// Process the callback
bool run_callback() const { bool run_callback() const {
return callback(results); return callback(results);
@ -145,7 +189,7 @@ public:
return sname==other.sname || lname==other.lname; return sname==other.sname || lname==other.lname;
} }
std::string getName() const { std::string get_name() const {
if(sname=="") if(sname=="")
return "--" + lname; return "--" + lname;
else if (lname=="") else if (lname=="")
@ -154,6 +198,10 @@ public:
return "-" + sname + ", --" + lname; return "-" + sname + ", --" + lname;
} }
bool check_name(const std::string& name) const {
return name == sname || name == lname || name == sname + "," + lname;
}
bool check_sname(const std::string& name) const { bool check_sname(const std::string& name) const {
return name == sname; return name == sname;
} }
@ -170,24 +218,23 @@ public:
return lname; return lname;
} }
int get_num() const {
return opts.num;
}
void add_result(int r, std::string s) { void add_result(int r, std::string s) {
logit("Adding result: " + s);
results.at(r).push_back(s); results.at(r).push_back(s);
} }
int get_new() { int get_new() {
results.emplace_back(); results.emplace_back();
return results.size() - 1; return results.size() - 1;
} }
int count() { int count() const {
return results.size(); int out = 0;
for(const std::vector<std::string>& v : results)
out += v.size();
return out;
} }
std::string string() const { std::string string() const {
std::string val = "Option: " + getName() + "\n" std::string val = "Option: " + get_name() + "\n"
+ " " + discription + "\n" + " " + discription + "\n"
+ " ["; + " [";
for(const auto& item : results) { for(const auto& item : results) {
@ -199,8 +246,58 @@ public:
return val; return val;
} }
int help_len() const {
return std::min((int) get_name().length(), 40);
}
std::string help(int len = 0) const {
std::stringstream out;
out << std::setw(len) << std::left << get_name() << discription;
return out.str();
}
}; };
template<typename T>
typename std::enable_if<std::is_integral<T>::value, bool>::type
lexical_cast(std::string input, T& output) {
logit("Int lexical cast " + input);
try{
output = (T) std::stoll(input);
return true;
} catch (std::invalid_argument) {
return false;
} catch (std::out_of_range) {
return false;
}
}
template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, bool>::type
lexical_cast(std::string input, T& output) {
logit("Floating lexical cast " + input);
try{
output = (T) std::stold(input);
return true;
} catch (std::invalid_argument) {
return false;
} catch (std::out_of_range) {
return false;
}
}
// String and similar
template<typename T>
bool lexical_cast(std::string input, T& output) {
logit("Direct lexical cast: " + input);
output = input;
return true;
}
enum class Classifer {NONE, POSITIONAL_MARK, SHORT, LONG, SUBCOMMAND};
/// Creates a command line program, with very few defaults. /// Creates a command line program, with very few defaults.
/** To use, create a new Program() instance with argc, argv, and a help discription. The templated /** To use, create a new Program() instance with argc, argv, and a help discription. The templated
* add_option methods make it easy to prepare options. Remember to call `.start` before starting your * add_option methods make it easy to prepare options. Remember to call `.start` before starting your
@ -210,19 +307,31 @@ public:
protected: protected:
std::string desc; std::string name;
std::string discription;
std::vector<Option> options; std::vector<Option> options;
std::vector<std::string> missing_options; std::vector<std::string> missing_options;
std::vector<std::string> positionals; std::vector<std::string> positionals;
std::vector<App> subcommands;
bool parsed{false};
App* subcommand = nullptr;
public: public:
/// Create a new program. Pass in the same arguments as main(), along with a help string. /// Create a new program. Pass in the same arguments as main(), along with a help string.
App(std::string discription) App(std::string discription="")
: desc(discription) { : discription(discription) {
add_flag("h,help", "Print this help message and exit");
} }
App& add_subcommand(std::string name) {
subcommands.emplace_back();
subcommands.back().name = name;
return subcommands.back();
}
/// Add an option, will automatically understand the type for common types. /// Add an option, will automatically understand the type for common types.
/** To use, create a variable with the expected type, and pass it in after the name. /** To use, create a variable with the expected type, and pass it in after the name.
* After start is called, you can use count to see if the value was passed, and * After start is called, you can use count to see if the value was passed, and
@ -242,23 +351,27 @@ public:
std::string discription="", ///< Discription string std::string discription="", ///< Discription string
Combiner opts=ARGS ///< The options (REQUIRED, DEFAULT, POSITIONAL, ARGS()) Combiner opts=ARGS ///< The options (REQUIRED, DEFAULT, POSITIONAL, ARGS())
) { ) {
Option myopt{name, discription, opts, callback}; Option myopt{name, discription, opts, callback};
if(std::find(std::begin(options), std::end(options), myopt) == std::end(options)) if(std::find(std::begin(options), std::end(options), myopt) == std::end(options))
options.push_back(myopt); options.push_back(myopt);
else else
throw OptionAlreadyAdded(myopt.getName()); throw OptionAlreadyAdded(myopt.get_name());
} }
/// Add option for string /// Add option for string
void add_option( template<typename T>
typename std::enable_if<!std::is_array<T>::value, void>::type
add_option(
std::string name, ///< The name, long,short std::string name, ///< The name, long,short
std::string &variable, ///< The variable to set T &variable, ///< The variable to set
std::string discription="", ///< Discription string std::string discription="", ///< Discription string
Combiner opts=ARGS ///< The options (REQUIRED, DEFAULT, POSITIONAL, ARGS()) Combiner opts=ARGS ///< The options (REQUIRED, DEFAULT, POSITIONAL, ARGS())
) { ) {
if(opts.num!=1)
throw IncorrectConstruction("Must have ARGS(1) or be a vector.");
CLI::callback_t fun = [&variable](CLI::results_t res){ CLI::callback_t fun = [&variable](CLI::results_t res){
if(res.size()!=1) { if(res.size()!=1) {
return false; return false;
@ -266,8 +379,33 @@ public:
if(res[0].size()!=1) { if(res[0].size()!=1) {
return false; return false;
} }
variable = res[0][0]; return lexical_cast(res[0][0], variable);
return true; };
add_option(name, fun, discription, opts);
}
/// Add option for vector of results
template<typename T>
void add_option(
std::string name, ///< The name, long,short
std::vector<T> &variable, ///< The variable to set
std::string discription="", ///< Discription string
Combiner opts=ARGS ///< The options (REQUIRED, DEFAULT, POSITIONAL, ARGS())
) {
if(opts.num==0)
throw IncorrectConstruction("Must have ARGS(1) or be a vector.");
CLI::callback_t fun = [&variable](CLI::results_t res){
bool retval = true;
int count = 0;
variable.clear();
for(const auto &a : res)
for(const auto &b : a) {
variable.emplace_back();
retval &= lexical_cast(b, variable.back());
}
return count != 0 && retval;
}; };
add_option(name, fun, discription, opts); add_option(name, fun, discription, opts);
@ -279,7 +417,6 @@ public:
std::string name, ///< The name, short,long std::string name, ///< The name, short,long
std::string discription="" ///< Discription string std::string discription="" ///< Discription string
) { ) {
CLI::callback_t fun = [](CLI::results_t res){ CLI::callback_t fun = [](CLI::results_t res){
return true; return true;
}; };
@ -298,141 +435,244 @@ public:
count = 0; count = 0;
CLI::callback_t fun = [&count](CLI::results_t res){ CLI::callback_t fun = [&count](CLI::results_t res){
count = res.size(); count = (T) res.size();
return true; return true;
}; };
add_option(name, fun, discription, NOTHING); add_option(name, fun, discription, NOTHING);
} }
/// Add set of options
template<typename T>
void add_set(
std::string name, ///< The name, short,long
T &member, ///< The selected member of the set
std::unordered_set<T> options, ///< The set of posibilities
std::string discription="", ///< Discription string
Combiner opts=ARGS ///< The options (REQUIRED, DEFAULT, POSITIONAL, ARGS())
) {
CLI::callback_t fun = [&member, options](CLI::results_t res){
if(res.size()!=1) {
return false;
}
if(res[0].size()!=1) {
return false;
}
bool retval = lexical_cast(res[0][0], member);
if(!retval)
return false;
return std::find(std::begin(options), std::end(options), retval) != std::end(options);
};
add_option(name, fun, discription, opts);
}
/// Parses the command line - throws errors /// Parses the command line - throws errors
void parse(int argc, char **argv) throw(CallForHelp, ParseError) { void parse(int argc, char **argv) {
std::vector<std::string> args; std::vector<std::string> args;
for(int i=1; i<argc; i++) for(int i=1; i<argc; i++)
args.emplace_back(argv[i]); args.emplace_back(argv[i]);
parse(args); parse(args);
} }
void parse(std::vector<std::string> args) throw(CallForHelp, ParseError) { void parse(std::vector<std::string> args) {
parsed = true;
std::reverse(args.begin(), args.end()); std::reverse(args.begin(), args.end());
bool positional_only = false; bool positional_only = false;
while(args.size()>0) { while(args.size()>0) {
if(args.back() == "--") { logit(join(args));
Classifer classifer = positional_only ? Classifer::NONE : _recognize(args.back());
switch(classifer) {
case Classifer::POSITIONAL_MARK:
args.pop_back(); args.pop_back();
positional_only = true; positional_only = true;
} else if(positional_only || (!_parse_long(args) && !_parse_short(args))) { break;
case Classifer::SUBCOMMAND:
_parse_subcommand(args);
break;
case Classifer::LONG:
_parse_long(args);
break;
case Classifer::SHORT:
_parse_short(args);
break;
case Classifer::NONE:
logit("Positional: "+args.back()); logit("Positional: "+args.back());
positionals.push_back(args.back()); positionals.push_back(args.back());
args.pop_back(); args.pop_back();
} }
} }
for(Option& opt : options) if (count("help") > 0) {
throw CallForHelp();
}
bool success = true;
for(Option& opt : options) {
while (opt.positional() && opt.count() < opt.expected() && positionals.size() > 0) {
opt.get_new();
opt.add_result(0, positionals.back());
positionals.pop_back();
}
if (opt.required() && opt.count() < opt.expected())
throw RequiredError(opt.get_name());
if (opt.count() > 0) { if (opt.count() > 0) {
logit(opt.string()); success &= opt.run_callback();
if(!opt.run_callback())
logit("Failed");
} }
//TODO: Check for false callbacks }
if(!success)
throw ParseError();
if(positionals.size()>0)
throw ExtraPositionalsError();
} }
bool _parse_short(std::vector<std::string> &args) { void _parse_subcommand(std::vector<std::string> &args) {
for(App &com : subcommands) {
if(com.name == args.back()){
args.pop_back();
com.parse(args);
subcommand = &com;
return;
}
}
throw HorribleError("Subcommand");
}
void _parse_short(std::vector<std::string> &args) {
std::string current = args.back(); std::string current = args.back();
std::smatch match; std::smatch match;
if(!std::regex_match(current, match, reg_short)) if(!std::regex_match(current, match, reg_short))
return false; throw HorribleError("Subcommand");
args.pop_back(); args.pop_back();
std::string name = match[1]; std::string name = match[1];
std::string rest = match[2]; std::string rest = match[2];
logit("Working on short:"); logit("Working on short: " + name + " (" + rest + ")");
logit(name);
logit(rest);
auto op = std::find_if(std::begin(options), std::end(options), [name](const Option &v){return v.check_sname(name);}); auto op = std::find_if(std::begin(options), std::end(options), [name](const Option &v){return v.check_sname(name);});
if(op == std::end(options)) { if(op == std::end(options)) {
missing_options.push_back("-" + op->get_sname()); missing_options.push_back("-" + op->get_sname());
return true; return;
} }
int vnum = op->get_new(); int vnum = op->get_new();
int num = op->get_num(); int num = op->expected();
if(num == 0)
if(rest != "" && num > 0) { op->add_result(vnum, "");
num--; else if(rest!="") {
if (num > 0)
num--;
op->add_result(vnum, rest); op->add_result(vnum, rest);
rest = ""; rest = "";
} }
while(num>0) {
if(num == -1) {
std::string current = args.back();
while(_recognize(current) == Classifer::NONE) {
std::string current = args.back();
args.pop_back();
op->add_result(vnum,current);
if(args.size()==0)
return;
}
} else while(num>0) {
num--; num--;
std::string current = args.back(); std::string current = args.back();
logit("Adding: "+current); logit("Adding: "+current);
args.pop_back(); args.pop_back();
op->add_result(vnum,current); op->add_result(vnum,current);
if(args.size()==0) if(args.size()==0)
return true; return;
} }
if(rest != "") { if(rest != "") {
rest = "-" + rest; rest = "-" + rest;
args.push_back(rest); args.push_back(rest);
} }
return true;
} }
bool _parse_long(std::vector<std::string> &args) { Classifer _recognize(std::string current) const {
if(current == "--")
return Classifer::POSITIONAL_MARK;
for(const App &com : subcommands) {
if(com.name == current)
return Classifer::SUBCOMMAND;
}
if(std::regex_match(current, reg_long))
return Classifer::LONG;
if(std::regex_match(current, reg_short))
return Classifer::SHORT;
return Classifer::NONE;
}
void _parse_long(std::vector<std::string> &args) {
std::string current = args.back(); std::string current = args.back();
std::smatch match; std::smatch match;
if(!std::regex_match(current, match, reg_long)) if(!std::regex_match(current, match, reg_long))
return false; throw HorribleError("Long");
args.pop_back(); args.pop_back();
std::string name = match[1]; std::string name = match[1];
std::string value = match[2]; std::string value = match[2];
logit("Working on long:"); logit("Working on long: " + name + " (" + value + ")");
logit(name);
logit(value);
auto op = std::find_if(std::begin(options), std::end(options), [name](const Option &v){return v.check_lname(name);}); auto op = std::find_if(std::begin(options), std::end(options), [name](const Option &v){return v.check_lname(name);});
if(op == std::end(options)) { if(op == std::end(options)) {
missing_options.push_back("--" + op->get_lname()); missing_options.push_back("--" + op->get_lname());
return true; return;
} }
int vnum = op->get_new(); int vnum = op->get_new();
int num = op->get_num(); int num = op->expected();
if(value != "") { if(value != "") {
num--; if(num!=-1) num--;
op->add_result(vnum, value); op->add_result(vnum, value);
} else if (num == 0) {
op->add_result(vnum, "");
} }
while(num>0) { if(num == -1) {
std::string current = args.back();
while(_recognize(current) == Classifer::NONE) {
std::string current = args.back();
args.pop_back();
op->add_result(vnum,current);
if(args.size()==0)
return;
}
} else while(num>0) {
num--; num--;
std::string current = args.back(); std::string current = args.back();
args.pop_back(); args.pop_back();
op->add_result(vnum,current); op->add_result(vnum,current);
if(args.size()==0) if(args.size()==0)
return true; return;
} }
return true; return;
} }
/// This must be called after the options are in but before the rest of the program. /// This must be called after the options are in but before the rest of the program.
@ -444,7 +684,7 @@ public:
} catch(const CallForHelp &e) { } catch(const CallForHelp &e) {
std::cout << help() << std::endl; std::cout << help() << std::endl;
exit(0); exit(0);
} catch(const ParseError &e) { } catch(const Error &e) {
std::cerr << "ERROR:" << std::endl; std::cerr << "ERROR:" << std::endl;
std::cerr << e.what() << std::endl; std::cerr << e.what() << std::endl;
std::cerr << help() << std::endl; std::cerr << help() << std::endl;
@ -455,11 +695,43 @@ public:
/// Counts the number of times the given option was passed. /// Counts the number of times the given option was passed.
int count(std::string name) const { int count(std::string name) const {
return 0; for(const Option &opt : options) {
logit("Computing: " + opt.get_name());
if(opt.check_name(name)) {
logit("Counting: " + opt.get_name() + std::to_string(opt.count()));
return opt.count();
}
}
throw OptionNotFound(name);
} }
std::string help() const {return "";} std::string help() const {
std::stringstream out;
out << discription << std::endl;
int len = std::accumulate(std::begin(options), std::end(options), 0,
[](int val, const Option &opt){
return std::max(opt.help_len(), val);});
for(const Option &opt : options) {
out << opt.help(len) << std::endl;
}
if(subcommands.size()> 0) {
out << "Subcommands: ";
for(const App &com : subcommands) {
if(&com != &subcommands[0])
out << ", ";
std::cout << com.get_name();
}
out << std::endl;
}
return out.str();
}
App* get_subcommand() {
return subcommand;
}
std::string get_name() const {
return name;
}
}; };
} }

View File

@ -9,12 +9,12 @@ int main (int argc, char** argv) {
app.add_option("f,file", file, "File name"); app.add_option("f,file", file, "File name");
int count; int count;
app.add_flag<int>("c,count", count, "File name"); app.add_flag("c,count", count, "File name");
app.parse(argc, argv); app.start(argc, argv);
std::cout << "Working on file: " << file << std::endl; std::cout << "Working on file: " << file << ", direct count: " << app.count("file") << std::endl;
std::cout << "Working on count: " << count << std::endl; std::cout << "Working on count: " << count << ", direct count: " << app.count("count") << std::endl;
return 0; return 0;
} }