From ea1f7ef663899e799001f50055cc93c733a9fff7 Mon Sep 17 00:00:00 2001 From: Sean Robinson Date: Tue, 26 Oct 2021 12:58:24 -0700 Subject: [PATCH] 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()); +}