diff --git a/README.md b/README.md index 327950b..be262fd 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,13 @@ int main(int argc, char **argv) { bool verbose = false; std::string toPrint; std::string maybePrint; - int repeats = 1; + int repeats = 2; // Inform the parser of the program data. It will do type-specific conversion // from string. // An option invoked with `--repeat N` - p.add_option(repeats, "--repeat"); + p.add_option(repeats, "--repeat")->help("how many times to repeat first argument"); // A flag invoked with `--verbose` or `-v` p.add_flag(verbose, "--verbose", "-v"); // a required positional argument (position 1) @@ -65,25 +65,29 @@ int main(int argc, char **argv) { ## Adding Options -Options may be `int`, `size_t`, `float`, `double`, or `std::string` and currently support long strings. -They are invoked like `--long-opt value` (not `--long-opt=value`). +Options may be `int`, `size_t`, `float`, `double`, or `std::string`. +They are invoked like `--long-opt value` (not `--long-opt=value`) or `-s value`, if provided. +If they are not present, the value is not modified. ```c++ argparse::Parser p; -p.add_option(var1, "--long-opt") +p.add_option(var1, "--long-opt"); +p.add_option(var2, "--long-opt, -s"); +p.add_option(var3, "--long-opt, -s")->help("a demo option"); ``` ## Adding Flags Flags are always `bool`s, and currently support long or short strings. The boolean variable is ALWAYS set to `true` if the flag is found. -They are invoked like `--long-flag` (not `--long-flag=true` or `--long-flag true`). +They are invoked like `--long-flag` (not `--long-flag=true` or `--long-flag true`) or `-s`, if provided. ```c++ argparse::Parser p; p.add_option(flag1, "--long-flag") -p.add_option(flag2, "--antother-flag", "-s"); +p.add_option(flag2, "--another-flag", "-s"); +p.add_option(flag3, "--another-flag", "-s")->help("a demo flag"); ``` ## Positional Arguments @@ -102,7 +106,7 @@ if (something->found()) { } ``` -Anything after `--` is considered a positional argument, unless -- follows an option. +Anything after `--` is considered a positional argument, unless `--` follows an option. Here, the first `--` is the value for `--option` and will be in `s1`. The second `--` marks the beginning of positional arguments. and the string `aa` will be in `s2`. @@ -150,11 +154,11 @@ To error instead, call `p->no_unrecognized()`. - [x] `float` - [x] `double` - [x] `std::string` +- [x] Support short option strings +- [x] Help string output ## Roadmap - [ ] Reject duplicate flags / options at run time -- [ ] Support short option strings -- [ ] Help string output - [ ] support --long-option=value - [ ] have the last positional argument fill a vector with remaining diff --git a/examples/example1.cpp b/examples/example1.cpp index b2e7c42..fb7b0eb 100644 --- a/examples/example1.cpp +++ b/examples/example1.cpp @@ -9,13 +9,13 @@ int main(int argc, char **argv) { bool verbose = false; std::string toPrint; std::string maybePrint; - int repeats = 1; + int repeats = 2; // Inform the parser of the program data. It will do type-specific conversion // from string. // An option invoked with `--repeat N` - p.add_option(repeats, "--repeat"); + p.add_option(repeats, "--repeat")->help("how many times to repeat first argument"); // A flag invoked with `--verbose` or `-v` p.add_flag(verbose, "--verbose", "-v"); // a required positional argument (position 1) diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index 2e46431..a9e616a 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -7,22 +7,70 @@ namespace argparse { +namespace detail { +template const char *type_str(); +template <> const char *type_str() { return "INT32"; } +template <> const char *type_str() { return "INT64"; } +template <> const char *type_str() { return "STR"; } +template <> const char *type_str() { return "F32"; } +template <> const char *type_str() { return "F64"; } +template <> const char *type_str() { return "SIZE_T"; } +} // namespace detail + +class Parser; + +/* Interface for an object representing a command-line option + */ class OptionBase { + friend class Parser; + public: - virtual void set_val(const std::string &valStr) = 0; + /* return the short and long options + */ + virtual const std::string &short_str() = 0; virtual const std::string &long_str() = 0; + + /* return the help string + */ + virtual const std::string &help_str() = 0; + + /* set help string + */ + virtual void help(const std::string &s) = 0; + +private: + /* a string representing the type of the option (INT, STR, FLOAT) + */ + virtual const char *type_str() = 0; + + /* set the value provided to this option + */ + virtual void set_val(const std::string &valStr) = 0; }; template class Option : public OptionBase { + std::string short_; std::string long_; + std::string help_; T *val_; public: - Option(T &val, const std::string &l) : long_(l), val_(&val) {} - void set_val(const std::string &val) override { set_val((T *)nullptr, val); } + Option(T &val, const std::string &l, const std::string &s = "") + : long_(l), short_(s), val_(&val) {} const std::string &long_str() override { return long_; } + const std::string &short_str() override { return short_; } + const std::string &help_str() override { return help_; } + + void help(const std::string &s) { help_ = s; } private: + /* uses a T* = nullpointer to disambiguate the call + */ + const char *type_str() override { return detail::type_str(); } + void set_val(const std::string &val) override { set_val((T *)nullptr, val); } + + /* how to parse supported types + */ void set_val(size_t *, const std::string &val) { // convert to size_t *val_ = std::stoull(val); } @@ -40,6 +88,8 @@ private: } }; +/* Represents a command-line flag + */ class Flag { std::string long_; std::string short_; @@ -61,11 +111,16 @@ public: }; class PosnlBase { + friend class Parser; + public: virtual bool is_required() = 0; virtual PosnlBase *required() = 0; virtual void set_val(const std::string &val) = 0; virtual bool found() = 0; + +private: + virtual const char *type_str() = 0; }; template class Positional : public PosnlBase { @@ -93,7 +148,8 @@ public: bool found() override { return found_; } private: - // https://stackoverflow.com/questions/5512910/explicit-specialization-of-template-class-member-function + const char *type_str() override { return detail::type_str(); } + template void get_as(C *, const std::string &val) { // to be overridden } @@ -125,6 +181,8 @@ class Parser { std::vector flags_; std::vector posnls_; + std::string argv0_; // will hold the name of the most recent parse(...) + static bool starts_with(const std::string &s, const std::string &prefix) { if (s.rfind(prefix, 0) == 0) { return true; @@ -134,8 +192,10 @@ class Parser { OptionBase *match_opt(const char *arg) const { std::string sarg(arg); + + // match options back-to-front to prefer later duplicates for (int64_t i = int64_t(opts_.size()) - 1; i >= 0; --i) { - if (opts_[i]->long_str() == sarg) { + if (opts_[i]->long_str() == sarg || opts_[i]->short_str() == sarg) { return opts_[i]; } } @@ -144,6 +204,8 @@ class Parser { Flag *match_flag(const char *arg) { std::string sarg(arg); + + // match flags back-to-front to prefer later duplicates for (int64_t i = int64_t(flags_.size()) - 1; i >= 0; --i) { if (flags_[i].long_str() == sarg || flags_[i].short_str() == sarg) { return &flags_[i]; @@ -153,7 +215,9 @@ class Parser { } public: - Parser() : noUnrecognized_(false), help_(false), consume_(true) { + Parser() + : description_("an argparse-powered program"), noUnrecognized_(false), + help_(false), consume_(true) { add_flag(help_, "--help", "-h")->help("Print help message"); } Parser(const std::string &description) @@ -167,6 +231,12 @@ public: std::vector newArgv; if (argc > 0) { newArgv.push_back(argv[0]); + + if (argv[0]) { + argv0_ = argv[0]; + } else { + argv0_ = ""; + } } size_t pi = 0; // positional argument position @@ -176,9 +246,10 @@ public: // try interpreting as a flag or option if it looks like one if (optsOkay && starts_with(argv[i], "-")) { // '--' indicates only positional arguments follow + // the second '--' should be interpreted as a positional argument if (argv[i] == std::string("--")) { - optsOkay = false; - continue; + optsOkay = false; + continue; } OptionBase *opt = match_opt(argv[i]); if (opt) { @@ -225,8 +296,11 @@ public: return true; }; - template void add_option(T &val, const std::string &l) { - opts_.push_back(new Option(val, l)); + template + OptionBase *add_option(T &val, const std::string &l, + const std::string &s = "") { + opts_.push_back(new Option(val, l, s)); + return opts_.back(); } Flag *add_flag(bool &val, const std::string &l, const std::string &s = "") { @@ -242,10 +316,26 @@ public: std::string help() const { std::stringstream ss; + ss << "Usage: " << argv0_ << " [OPTION]..."; + + for (auto &p : posnls_) { + ss << " "; + ss << (p->is_required() ? "" : "["); + ss << p->type_str(); + ss << (p->is_required() ? "" : "]"); + } + ss << "\n"; + ss << description_ << "\n"; for (auto &o : opts_) { - ss << o->long_str() << "\n"; + ss << " "; + if (!o->short_str().empty()) { + ss << o->short_str() << ", "; + } + ss << o->long_str() << "\t" << o->type_str(); + ss << "\t\t" << o->help_str(); + ss << "\n"; } for (auto &f : flags_) { diff --git a/test/test_argparse.cpp b/test/test_argparse.cpp index 4b42663..a549bbc 100644 --- a/test/test_argparse.cpp +++ b/test/test_argparse.cpp @@ -6,6 +6,20 @@ TEST_CASE("argparse") { + SECTION("no args") { + char ** argv = nullptr; + int argc = 0; + argparse::Parser p; + REQUIRE(p.parse(argc, argv)); + } + + SECTION("null argv[0]") { + char *argv[] = {nullptr}; + int argc = sizeof(argv) / sizeof(argv[0]); + argparse::Parser p; + REQUIRE(p.parse(argc, argv)); + } + SECTION("types") { char *argv[] = { "some-exe", "--campi", "--f", "10", "1.7", "1.8", @@ -42,13 +56,6 @@ TEST_CASE("argparse") { REQUIRE(argc == 2); // does not use --f or some-exe } - SECTION("no args") { - char *argv[] = {nullptr}; - int argc = sizeof(argv) / sizeof(argv[0]); - argparse::Parser p; - REQUIRE(p.parse(argc, argv)); - } - SECTION("description") { char *argv[] = { @@ -95,23 +102,15 @@ TEST_CASE("argparse") { char *argv[] = {"some-exe", "-h"}; int argc = sizeof(argv) / sizeof(argv[0]); - std::string a; - std::string b; - argparse::Parser p; REQUIRE(true == p.parse(argc, argv)); REQUIRE(p.need_help()); - - std::cerr << p.help() << "\n"; } SECTION("--help") { char *argv[] = {"some-exe", "--help"}; int argc = sizeof(argv) / sizeof(argv[0]); - std::string a; - std::string b; - argparse::Parser p; REQUIRE(true == p.parse(argc, argv)); REQUIRE(p.need_help()); @@ -121,12 +120,50 @@ TEST_CASE("argparse") { char *argv[] = {"some-exe", "--help", "--"}; int argc = sizeof(argv) / sizeof(argv[0]); - std::string a; - std::string b; - argparse::Parser p; REQUIRE(true == p.parse(argc, argv)); REQUIRE(p.need_help()); } + SECTION("double --") { + char *argv[] = {"some-exe", "--flag", "--", "--", "aa"}; + int argc = sizeof(argv) / sizeof(argv[0]); + + std::string a,b; + bool flag = false; + + argparse::Parser p; + p.add_flag(flag, "--flag"); + p.add_positional(a); + p.add_positional(b); + + + REQUIRE(true == p.parse(argc, argv)); + REQUIRE(!p.need_help()); + + REQUIRE(a == "--"); + REQUIRE(b == "aa"); + REQUIRE(flag == true); + + } + + SECTION("-- in option") { + char *argv[] = {"some-exe", "--option", "--", "--", "aa"}; + int argc = sizeof(argv) / sizeof(argv[0]); + + std::string a, option; + + argparse::Parser p; + p.add_option(option, "--option"); + p.add_positional(a); + + + REQUIRE(true == p.parse(argc, argv)); + REQUIRE(!p.need_help()); + + REQUIRE(a == "aa"); + REQUIRE(option == "--"); + + } + }