Skip to content

Commit

Permalink
Allow removal of default arguments (i.e. --help and --version)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
skrobinson committed Oct 27, 2021
1 parent 2b05334 commit ea1f7ef
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 19 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
52 changes: 33 additions & 19 deletions include/argparse/argparse.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,17 @@ template <class T> struct parse_number<T, chars_format::fixed> {

} // 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<unsigned int>(a) & static_cast<unsigned int>(b);
}

class ArgumentParser;

class Argument {
Expand Down Expand Up @@ -814,16 +825,31 @@ class Argument {
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
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;
Expand Down Expand Up @@ -1051,18 +1077,6 @@ class ArgumentParser {
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] == '-' &&
Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions test/test_default_args.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include <argparse/argparse.hpp>
#include <doctest.hpp>

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);
}
24 changes: 24 additions & 0 deletions test/test_help.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <argparse/argparse.hpp>
#include <doctest.hpp>
#include <sstream>

using doctest::test_suite;

Expand Down Expand Up @@ -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());
}
26 changes: 26 additions & 0 deletions test/test_version.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <doctest.hpp>
#include <argparse/argparse.hpp>
#include <sstream>

using doctest::test_suite;

Expand All @@ -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());
}

0 comments on commit ea1f7ef

Please sign in to comment.