Last active
November 14, 2017 18:47
-
-
Save NiklasRosenstein/cec35b313e317f23216d29ff2ef1b42a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* This is free and unencumbered software released into the public domain. | |
* | |
* Anyone is free to copy, modify, publish, use, compile, sell, or | |
* distribute this software, either in source code form or as a compiled | |
* binary, for any purpose, commercial or non-commercial, and by any | |
* means. | |
* | |
* In jurisdictions that recognize copyright laws, the author or authors | |
* of this software dedicate any and all copyright interest in the | |
* software to the public domain. We make this dedication for the benefit | |
* of the public at large and to the detriment of our heirs and | |
* successors. We intend this dedication to be an overt act of | |
* relinquishment in perpetuity of all present and future rights to this | |
* software under copyright law. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
* IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR | |
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, | |
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
* OTHER DEALINGS IN THE SOFTWARE. | |
* | |
* For more information, please refer to <http://unlicense.org> | |
*/ | |
#include <algorithm> | |
#include <cassert> | |
#include <iostream> | |
#include <unordered_map> | |
#include <string> | |
#include <vector> | |
/* This class implements a simple command-line argument parser. */ | |
class argument_parser { | |
public: | |
/* Settings for the argument parser. */ | |
struct settings_t { | |
std::string short_option_prefix = "-"; | |
std::string long_option_prefix = "--"; | |
}; | |
/* A structure that represents an option. */ | |
struct option_info_t { | |
std::string short_name; | |
std::string long_name; | |
int argc; | |
bool required; | |
std::string const& map_key() const { return long_name.empty() ? short_name : long_name; } | |
}; | |
/* A structure that represents a positional argument. Every positional | |
* argument is stored in a #std::vector of arguments, even if the number | |
* of consumed positional arguments is fixed to 1. */ | |
struct argument_info_t { | |
std::string name; | |
int argc; | |
bool required; | |
}; | |
/* Raised when the command-line settings did not fulfill all requirements. */ | |
struct parse_error : public std::exception { | |
std::string _msg; | |
parse_error(std::string const& msg) : _msg(msg) {} | |
virtual char const* what() const { return _msg.c_str(); } | |
}; | |
using arglist_t = std::vector<std::string>; | |
using results_t = std::unordered_map<std::string, arglist_t>; | |
/* Check if a string starts with another string. */ | |
static bool startswith(std::string const& subject, std::string const& needle) { | |
if (subject.size() < needle.size()) return false; | |
return strncmp(subject.c_str(), needle.c_str(), needle.size()) == 0; | |
} | |
/* Converts a string to uppercase. */ | |
static std::string toupper(std::string str) { | |
std::transform(str.begin(), str.end(), str.begin(), ::toupper); | |
return str; | |
} | |
private: // members | |
std::string _name; | |
results_t _parsed_args; | |
std::vector<option_info_t> _options; | |
std::vector<argument_info_t> _args; | |
settings_t _settings; | |
std::string _format_option_name(option_info_t const& opt) const { | |
std::string res; | |
if (!opt.short_name.empty()) { | |
res += _settings.short_option_prefix + opt.short_name; | |
if (!opt.long_name.empty()) { | |
res += ", "; | |
} | |
} | |
if (!opt.long_name.empty()) { | |
res += _settings.long_option_prefix + opt.long_name; | |
} | |
return res; | |
} | |
public: // members | |
argument_parser(std::string const& name, settings_t const& settings = {}) | |
: _name(name), _settings(settings) {} | |
~argument_parser() {} | |
/* Add an option to the parser. */ | |
void add_option( | |
std::string const& short_name, | |
std::string const& long_name, | |
int argc = 1, | |
bool required = false) | |
{ | |
_options.push_back({short_name, long_name, argc, required}); | |
} | |
/* Add a positional argument to the parser. An argument consumes as many | |
* values as possible if #argc is below 0. #argc can not be 0. */ | |
void add_argument( | |
std::string const& name, | |
int argc = 1, | |
bool required = true) | |
{ | |
assert(argc != 0); | |
_args.push_back({name, argc, required}); | |
} | |
/* Parse the command-line arguments. */ | |
void operator () (int argc, char** argv) { | |
// Consumes an argument and returns true on success, false on failure, | |
// that is, when there are no arguments left to be consumed. | |
auto consume = [&](std::string* result) -> bool { | |
if (argc <= 0) return false; | |
argc--; *result = *argv++; | |
return true; | |
}; | |
// True if options are still accepted. When the long_option_prefix is | |
// parsed on its own, options are no longer accepted and only positional | |
// arguments are expected. | |
bool accept_options = true; | |
// Iterator for the current argument that is being consumed into when an | |
// argument does not actually represent an option, plus the number of | |
// arguments already consumed for this argument definition. | |
auto arg_it = _args.begin(); | |
int args_consumed = 0; | |
// The current option that is being handled and the number of arguments | |
// that have already been consumed for the option. | |
option_info_t* current_option = nullptr; | |
int option_args_consumed = 0; | |
// Checks if the current option is satisfied, then switches to a new | |
// (or no) option. | |
auto next_option = [&](option_info_t* new_option) -> void { | |
if (current_option && option_args_consumed != current_option->argc) { | |
std::string name = (current_option->long_name.empty() ? | |
_settings.short_option_prefix + current_option->short_name : | |
_settings.long_option_prefix + current_option->long_name); | |
throw parse_error("option \"" + name + "\" requires " + | |
std::to_string(current_option->argc) + " arguments, but received " + | |
std::to_string(option_args_consumed)); | |
} | |
current_option = new_option; | |
option_args_consumed = 0; | |
}; | |
std::string current; | |
while (consume(¤t)) { | |
if (accept_options) { | |
// Skip options after the long_option_prefix was parsed alone. | |
if (current == _settings.long_option_prefix) { | |
accept_options = false; | |
continue; | |
} | |
// Check whether this is a short or long option name. | |
char option_type = 0; | |
std::string unprefixed_name; | |
if (startswith(current, _settings.long_option_prefix)) { | |
option_type = 2; | |
unprefixed_name = current.substr(_settings.long_option_prefix.size()); | |
} | |
else if (startswith(current, _settings.short_option_prefix)) { | |
option_type = 1; | |
unprefixed_name = current.substr(_settings.short_option_prefix.size()); | |
} | |
if (option_type != 0) { | |
// Find the option information matching this option name. | |
bool option_found = false; | |
for (auto& opt : _options) { | |
if ((option_type == 1 && opt.short_name == unprefixed_name) || | |
(option_type == 2 && opt.long_name == unprefixed_name)) { | |
next_option(&opt); | |
option_found = true; | |
break; | |
} | |
} | |
if (!option_found) { | |
throw parse_error("unknown option \"" + current + "\""); | |
} | |
if (current_option->argc == 0) { | |
// Create the entry. | |
_parsed_args[current_option->map_key()]; | |
next_option(nullptr); | |
} | |
continue; | |
} | |
} | |
if (current_option && option_args_consumed < current_option->argc) { | |
option_args_consumed++; | |
_parsed_args[current_option->map_key()].push_back(current); | |
continue; | |
} | |
if (arg_it == _args.end()) { | |
throw parse_error("positional argument could not be consumed: " + current); | |
} | |
_parsed_args[arg_it->name].push_back(current); | |
args_consumed++; | |
if (arg_it->argc > 0 && args_consumed >= arg_it->argc) { | |
arg_it++; | |
args_consumed = 0; | |
} | |
} | |
next_option(nullptr); | |
for (auto& opt : _options) { | |
if (opt.required && !has(opt.map_key())) { | |
throw parse_error("required option \"" + _format_option_name(opt) + | |
"\" is not specified."); | |
} | |
} | |
if (arg_it != _args.end() && arg_it->argc > 0) { | |
throw parse_error("missing required argument(s): " + arg_it->name); | |
} | |
} | |
/* Prints the help for this argument parser. */ | |
void print_help() const { | |
std::cout << "usage: " << _name << " [OPTIONS]"; | |
for (auto& arg : _args) { | |
std::cout << " "; | |
if (!arg.required) std::cout << "["; | |
std::cout << toupper(arg.name); | |
if (!arg.required) std::cout << "]"; | |
} | |
std::cout << "\n\n"; | |
if (!_args.empty()) { | |
std::cout << "positional arguments:\n"; | |
for (auto& arg : _args) { | |
std::cout << " " << arg.name << "\n"; | |
} | |
std::cout << " " << _settings.long_option_prefix << "\n"; | |
std::cout << "\n"; | |
} | |
if (!_options.empty()) { | |
std::cout << "options:\n"; | |
} | |
for (auto& opt : _options) { | |
std::cout << " " << _format_option_name(opt) << "\n"; | |
} | |
} | |
/* Returns #true if the parser has a valid entry for the specified argument. */ | |
bool has(std::string const& key) const { | |
return _parsed_args.find(key) != _parsed_args.end(); | |
} | |
/* Returns the values for the specified argument. Does not fail, even if the | |
* value does not exist, however it can return an empty vector. Check with #has() | |
* if the argument or option was parsed. */ | |
arglist_t const& operator [] (std::string const& key) { | |
return _parsed_args[key]; | |
} | |
}; | |
// TEST | |
std::ostream& operator << (std::ostream& s, std::vector<std::string> const& v) { | |
s << "["; | |
for (auto& item : v) { | |
s << "\"" << item << "\","; | |
} | |
s << "]"; | |
return s; | |
} | |
int main(int argc, char** argv) { | |
argument_parser parser("test"); | |
parser.add_option("h", "help", 0); | |
parser.add_option("v", "verbose", 0, true); | |
parser.add_option("q", "quiet", 0); | |
parser.add_option("o", "output"); | |
parser.add_argument("config"); | |
parser.add_argument("input", -1); | |
try { | |
parser(argc-1, argv+1); | |
} | |
catch (argument_parser::parse_error& exc) { | |
std::cout << "error: " << exc.what() << "\n"; | |
return 1; | |
} | |
if (parser.has("help")) { | |
parser.print_help(); | |
return 0; | |
} | |
std::cout << "help: " << parser.has("help") << "\n"; | |
std::cout << "verbose: " << parser.has("verbose") << "\n"; | |
std::cout << "quiet: " << parser.has("quiet") << "\n"; | |
std::cout << "output: " << parser["output"] << "\n"; | |
std::cout << "config: " << parser["config"] << "\n"; | |
std::cout << "input: " << parser["input"] << "\n"; | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment