From 8772b37aabf9c3679c6b346c113fc7b82a247452 Mon Sep 17 00:00:00 2001 From: Sean Robinson Date: Tue, 26 Oct 2021 12:58:24 -0700 Subject: [PATCH 1/6] Update examples from exit() to std::exit() argparse seems to use the "std::" qualifier for std namespace members, continue that in the documentation. Signed-off-by: Sean Robinson --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index fa6f120..8df359f 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ int main(int argc, char *argv[]) { catch (const std::runtime_error& err) { std::cout << err.what() << std::endl; std::cout << program; - exit(0); + std::exit(0); } auto input = program.get("square"); @@ -103,7 +103,7 @@ try { catch (const std::runtime_error& err) { std::cout << err.what() << std::endl; std::cout << program; - exit(0); + std::exit(0); } if (program["--verbose"] == true) { @@ -169,7 +169,7 @@ try { catch (const std::runtime_error& err) { std::cout << err.what() << std::endl; std::cout << program; - exit(0); + std::exit(0); } auto color = program.get("--color"); // "orange" @@ -192,7 +192,7 @@ try { catch (const std::runtime_error& err) { std::cout << err.what() << std::endl; std::cout << program; - exit(0); + std::exit(0); } auto colors = program.get>("--color"); // {"red", "green", "blue"} @@ -222,7 +222,7 @@ try { catch (const std::runtime_error& err) { std::cout << err.what() << std::endl; std::cout << program; - exit(0); + std::exit(0); } // Some code to print arguments @@ -255,7 +255,7 @@ try { catch (const std::runtime_error& err) { std::cout << err.what() << std::endl; std::cout << program; - exit(0); + std::exit(0); } int input = program.get("square"); @@ -314,7 +314,7 @@ try { catch (const std::runtime_error& err) { std::cout << err.what() << std::endl; std::cout << program; - exit(0); + std::exit(0); } auto files = program.get>("--input_files"); // {"config.yml", "System.xml"} @@ -343,7 +343,7 @@ try { catch (const std::runtime_error& err) { std::cout << err.what() << std::endl; std::cout << program; - exit(0); + std::exit(0); } auto query_point = program.get>("--query_point"); // {3.5, 4.7, 9.2} @@ -375,7 +375,7 @@ try { catch (const std::runtime_error& err) { std::cout << err.what() << std::endl; std::cout << program; - exit(0); + std::exit(0); } auto a = program.get("-a"); // true @@ -459,7 +459,7 @@ try { catch (const std::runtime_error& err) { std::cout << err.what() << std::endl; std::cout << program; - exit(0); + std::exit(0); } try { @@ -506,7 +506,7 @@ try { catch (const std::runtime_error& err) { std::cout << err.what() << std::endl; std::cout << program; - exit(0); + std::exit(0); } auto output_filename = program.get("-o"); @@ -588,7 +588,7 @@ try { catch (const std::runtime_error& err) { std::cout << err.what() << std::endl; std::cout << program; - exit(0); + std::exit(0); } nlohmann::json config = program.get("config"); @@ -624,7 +624,7 @@ try { catch (const std::runtime_error& err) { std::cout << err.what() << std::endl; std::cout << program; - exit(0); + std::exit(0); } auto numbers = program.get>("numbers"); // {1, 2, 3} @@ -666,7 +666,7 @@ try { catch (const std::runtime_error& err) { std::cout << err.what() << std::endl; std::cout << program; - exit(0); + std::exit(0); } auto input = program.get("input"); From 58777d8c845f75bae2d5eae195084fd708066a3f Mon Sep 17 00:00:00 2001 From: Sean Robinson Date: Tue, 26 Oct 2021 12:58:24 -0700 Subject: [PATCH 2/6] Replace spaces with underscores in example program names These examples give a false impression that a space in the middle of the application name is well handled. While a space might be possible, it must be escaped in shells, i.e. a common environment for a CLI argument parser. Signed-off-by: Sean Robinson --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8df359f..fd8860f 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Simply include argparse.hpp and you're good to go. To start parsing command-line arguments, create an ```ArgumentParser```. ```cpp -argparse::ArgumentParser program("program name"); +argparse::ArgumentParser program("program_name"); ``` **NOTE:** There is an optional second argument to the `ArgumentParser` which is the program version. Example: `argparse::ArgumentParser program("libfoo", "1.9.0");` @@ -49,7 +49,7 @@ Here's an example of a ***positional argument***: #include int main(int argc, char *argv[]) { - argparse::ArgumentParser program("program name"); + argparse::ArgumentParser program("program_name"); program.add_argument("square") .help("display the square of a given integer") From 748bc95cf5881394680cdc91c8027984427a5e6f Mon Sep 17 00:00:00 2001 From: Sean Robinson Date: Tue, 26 Oct 2021 12:58:24 -0700 Subject: [PATCH 3/6] Rename inner scope variables to differ from outer scope These variables with the same name are not the same variables because of scope rules. While the compiler is not confused by this naming, it may be less readable by someone attempting to edit this code. Signed-off-by: Sean Robinson --- include/argparse/argparse.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index 75437f8..8a6dff8 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -453,18 +453,18 @@ public: struct action_apply { void operator()(valued_action &f) { - std::transform(start, end, std::back_inserter(self.mValues), f); + std::transform(first, last, std::back_inserter(self.mValues), f); } void operator()(void_action &f) { - std::for_each(start, end, f); + std::for_each(first, last, f); if (!self.mDefaultValue.has_value()) { if (auto expected = self.maybe_nargs()) self.mValues.resize(*expected); } } - Iterator start, end; + Iterator first, last; Argument &self; }; std::visit(action_apply{start, end, *this}, mAction); From 2b05334a3c37f471e4a26dbebd19ab5fee6e1aa4 Mon Sep 17 00:00:00 2001 From: Sean Robinson Date: Tue, 26 Oct 2021 12:58:24 -0700 Subject: [PATCH 4/6] Run Argumnet::action functor for zero-parameter arguments Previously, only arguments with one or more parameters would run actions. But, at times it can be useful to run an action when an argument does not expect any parameters. Closes #104 Signed-off-by: Sean Robinson --- README.md | 18 ++++++++++++++++++ include/argparse/argparse.hpp | 1 + test/test_actions.cpp | 24 ++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/README.md b/README.md index fd8860f..db53e64 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,24 @@ auto colors = program.get>("--color"); // {"red", "gre Notice that ```.default_value``` is given an explicit template parameter to match the type you want to ```.get```. +#### Repeating an argument to increase a value + +A common pattern is to repeat an argument to indicate a greater value. + +```cpp +int verbosity = 0; +program.add_argument("-V", "--verbose") + .action([&](const auto &) { ++verbosity; }) + .append() + .default_value(false) + .implicit_value(true) + .nargs(0); + +program.parse_args(argc, argv); // Example: ./main -VVVV + +std::cout << "verbose level: " << verbosity << std::endl; // verbose level: 4 +``` + ### Negative Numbers Optional arguments start with ```-```. Can ```argparse``` handle negative numbers? The answer is yes! diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index 8a6dff8..980ebc7 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -442,6 +442,7 @@ public: mUsedName = usedName; if (mNumArgs == 0) { mValues.emplace_back(mImplicitValue); + std::visit([](auto &aAction) { aAction({}); }, mAction); return start; } else if (mNumArgs <= std::distance(start, end)) { if (auto expected = maybe_nargs()) { diff --git a/test/test_actions.cpp b/test/test_actions.cpp index 6d624af..7c5fd10 100644 --- a/test/test_actions.cpp +++ b/test/test_actions.cpp @@ -133,3 +133,27 @@ TEST_CASE("Users can use actions on remaining arguments" * program.parse_args({"sum", "42", "100", "-3", "-20"}); REQUIRE(result == 119); } + +TEST_CASE("Users can run actions on parameterless optional arguments" * + test_suite("actions")) { + argparse::ArgumentParser program("test"); + + GIVEN("a flag argument with a counting action") { + int count = 0; + program.add_argument("-V", "--verbose") + .action([&](const auto &) { ++count; }) + .append() + .default_value(false) + .implicit_value(true) + .nargs(0); + + WHEN("the flag is repeated") { + program.parse_args({"test", "-VVVV"}); + + THEN("the count increments once per use") { + REQUIRE(program.get("-V")); + REQUIRE(count == 4); + } + } + } +} From ea1f7ef663899e799001f50055cc93c733a9fff7 Mon Sep 17 00:00:00 2001 From: Sean Robinson Date: Tue, 26 Oct 2021 12:58:24 -0700 Subject: [PATCH 5/6] Allow removal of default arguments (i.e. --help and --version) The help and version arguments are still included by default, but which default arguments to include can be overridden at ArgumentParser creation. argparse generally copies Python argparse behavior. This includes a default `--help`/`-h` argument to print a help message and exit. Some developers using argparse find the automatic exit to be undesirable. The Python argparse has an opt-out parameter when constructing an ArgumentParser. Using `add_help=False` avoids adding a default `--help` argument and allows the developer to implement a custom help. This commit adds a similar opt-out to our C++ argparse, but keeps the current behavior as the default. The `--help`/`-h` and `--version`/`-v` Arguments handle their own output and exit rather than specially treating them in ArgumentParser::parse_args_internal. Closes #119 Closes #138 Closes #139 Signed-off-by: Sean Robinson --- README.md | 21 ++++++++++++++ include/argparse/argparse.hpp | 54 ++++++++++++++++++++++------------- test/CMakeLists.txt | 1 + test/test_default_args.cpp | 19 ++++++++++++ test/test_help.cpp | 24 ++++++++++++++++ test/test_version.cpp | 26 +++++++++++++++++ 6 files changed, 125 insertions(+), 20 deletions(-) create mode 100644 test/test_default_args.cpp diff --git a/README.md b/README.md index db53e64..6801047 100644 --- a/README.md +++ b/README.md @@ -455,6 +455,27 @@ The grammar follows `std::from_chars`, but does not exactly duplicate it. For ex | 'u' | decimal (unsigned) | | 'x' or 'X' | hexadecimal (unsigned) | +### Default Arguments + +`argparse` provides predefined arguments and actions for `-h`/`--help` and `-v`/`--version`. These default actions exit the program after displaying a help or version message, respectively. These defaults arguments can be disabled during `ArgumentParser` creation so that you can handle these arguments in your own way. (Note that a program name and version must be included when choosing default arguments.) + +```cpp +argparse::ArgumentParser program("test", "1.0", default_arguments::none); + +program.add_argument("-h", "--help") + .action([=](const std::string& s) { + std::cout << help().str(); + }) + .default_value(false) + .help("shows help message") + .implicit_value(true) + .nargs(0); +``` + +The above code snippet outputs a help message and continues to run. It does not support a `--version` argument. + +The default is `default_arguments::all` for included arguments. No default arguments will be added with `default_arguments::none`. `default_arguments::help` and `default_arguments::version` will individually add `--help` and `--version`. + ### Gathering Remaining Arguments `argparse` supports gathering "remaining" arguments at the end of the command, e.g., for use in a compiler: diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index 980ebc7..cda03e5 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -312,6 +312,17 @@ template struct parse_number { } // namespace details +enum class default_arguments : unsigned int { + none = 0, + help = 1, + version = 2, + all = help | version, +}; + +inline bool operator& (const default_arguments &a, const default_arguments &b) { + return static_cast(a) & static_cast(b); +} + class ArgumentParser; class Argument { @@ -814,16 +825,31 @@ private: class ArgumentParser { public: explicit ArgumentParser(std::string aProgramName = {}, - std::string aVersion = "1.0") + std::string aVersion = "1.0", + default_arguments aArgs = default_arguments::all) : mProgramName(std::move(aProgramName)), mVersion(std::move(aVersion)) { - add_argument("-h", "--help").help("shows help message and exits").nargs(0); -#ifndef ARGPARSE_LONG_VERSION_ARG_ONLY - add_argument("-v", "--version") -#else - add_argument("--version") -#endif - .help("prints version information and exits") + if (aArgs & default_arguments::help) { + add_argument("-h", "--help") + .action([&](const auto &) { + std::cout << help().str(); + std::exit(0); + }) + .default_value(false) + .help("shows help message and exits") + .implicit_value(true) .nargs(0); + } + if (aArgs & default_arguments::version) { + add_argument("-v", "--version") + .action([&](const auto &) { + std::cout << mVersion; + std::exit(0); + }) + .default_value(false) + .help("prints version information and exits") + .implicit_value(true) + .nargs(0); + } } ArgumentParser(ArgumentParser &&) noexcept = default; @@ -1051,18 +1077,6 @@ private: auto tIterator = mArgumentMap.find(tCurrentArgument); if (tIterator != mArgumentMap.end()) { auto tArgument = tIterator->second; - - // the first optional argument is --help - if (tArgument == mOptionalArguments.begin()) { - std::cout << *this; - std::exit(0); - } - // the second optional argument is --version - else if (tArgument == std::next(mOptionalArguments.begin(), 1)) { - std::cout << mVersion << "\n"; - std::exit(0); - } - it = tArgument->consume(std::next(it), end, tIterator->first); } else if (const auto &tCompoundArgument = tCurrentArgument; tCompoundArgument.size() > 1 && tCompoundArgument[0] == '-' && diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e8f7d89..3c502d9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -30,6 +30,7 @@ file(GLOB ARGPARSE_TEST_SOURCES test_compound_arguments.cpp test_container_arguments.cpp test_const_correct.cpp + test_default_args.cpp test_get.cpp test_help.cpp test_invalid_arguments.cpp diff --git a/test/test_default_args.cpp b/test/test_default_args.cpp new file mode 100644 index 0000000..046b88e --- /dev/null +++ b/test/test_default_args.cpp @@ -0,0 +1,19 @@ +#include +#include + +using doctest::test_suite; + +TEST_CASE("Include all default arguments" * test_suite("default_args")) { + argparse::ArgumentParser parser("test"); + auto help_msg { parser.help().str() }; + REQUIRE(help_msg.find("shows help message") != std::string::npos); + REQUIRE(help_msg.find("prints version information") != std::string::npos); +} + +TEST_CASE("Do not include default arguments" * test_suite("default_args")) { + argparse::ArgumentParser parser("test", "1.0", + argparse::default_arguments::none); + parser.parse_args({"test"}); + REQUIRE_THROWS_AS(parser.get("--help"), std::logic_error); + REQUIRE_THROWS_AS(parser.get("--version"), std::logic_error); +} diff --git a/test/test_help.cpp b/test/test_help.cpp index 78438ea..e3bb225 100644 --- a/test/test_help.cpp +++ b/test/test_help.cpp @@ -1,5 +1,6 @@ #include #include +#include using doctest::test_suite; @@ -51,3 +52,26 @@ TEST_CASE("Users can override the help options" * test_suite("help")) { } } } + +TEST_CASE("Users can disable default -h/--help" * test_suite("help")) { + argparse::ArgumentParser program("test", "1.0", + argparse::default_arguments::version); + REQUIRE_THROWS_AS(program.parse_args({"test", "-h"}), std::runtime_error); +} + +TEST_CASE("Users can replace default -h/--help" * test_suite("help")) { + argparse::ArgumentParser program("test", "1.0", + argparse::default_arguments::version); + std::stringstream buffer; + program.add_argument("-h", "--help") + .action([&](const auto &) { + buffer << program; + }) + .default_value(false) + .implicit_value(true) + .nargs(0); + + REQUIRE(buffer.str().empty()); + program.parse_args({"test", "--help"}); + REQUIRE_FALSE(buffer.str().empty()); +} diff --git a/test/test_version.cpp b/test/test_version.cpp index 4f5487a..ad71b25 100644 --- a/test/test_version.cpp +++ b/test/test_version.cpp @@ -1,5 +1,6 @@ #include #include +#include using doctest::test_suite; @@ -11,3 +12,28 @@ TEST_CASE("Users can print version and exit" * test_suite("version") program.parse_args( { "test", "--version" }); REQUIRE(program.get("--version") == "1.9.0"); } + +TEST_CASE("Users can disable default -v/--version" * test_suite("version")) { + argparse::ArgumentParser program("test", "1.0", + argparse::default_arguments::help); + REQUIRE_THROWS_AS(program.parse_args({"test", "--version"}), + std::runtime_error); +} + +TEST_CASE("Users can replace default -v/--version" * test_suite("version")) { + std::string version { "3.1415" }; + argparse::ArgumentParser program("test", version, + argparse::default_arguments::help); + std::stringstream buffer; + program.add_argument("-v", "--version") + .action([&](const auto &) { + buffer << version; + }) + .default_value(true) + .implicit_value(false) + .nargs(0); + + REQUIRE(buffer.str().empty()); + program.parse_args({"test", "--version"}); + REQUIRE_FALSE(buffer.str().empty()); +} From 5cceb98e3c1f5cf23d3323853062e4b59086381b Mon Sep 17 00:00:00 2001 From: Sean Robinson Date: Tue, 26 Oct 2021 12:58:24 -0700 Subject: [PATCH 6/6] Update "Printing Help" documentation Help output has changed format over time. This updates the README example to reflect current practice by running the example code and copy-pasting its output. Signed-off-by: Sean Robinson --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6801047..f2561a3 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ As you can see here, ```argparse``` supports negative integers, negative floats ### Combining Positional and Optional Arguments ```cpp -argparse::ArgumentParser program("test"); +argparse::ArgumentParser program("main"); program.add_argument("square") .help("display the square of a given number") @@ -303,14 +303,15 @@ The square of 4 is 16 ``` $ ./main --help -Usage: ./main [options] square +Usage: main [options] square Positional arguments: -square display a square of a given number +square display the square of a given number Optional arguments: --h, --help show this help message and exit --v, --verbose enable verbose logging +-h --help shows help message and exits [default: false] +-v --version prints version information and exits [default: false] +--verbose [default: false] ``` You may also get the help message in string via `program.help().str()`.