diff --git a/README.md b/README.md index fa6f120..f2561a3 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") @@ -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"} @@ -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! @@ -222,7 +240,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 @@ -239,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") @@ -255,7 +273,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"); @@ -285,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()`. @@ -314,7 +333,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 +362,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 +394,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 @@ -437,6 +456,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: @@ -459,7 +499,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 +546,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 +628,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 +664,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 +706,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"); diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index 75437f8..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 { @@ -442,6 +453,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()) { @@ -453,18 +465,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); @@ -813,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; @@ -1050,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_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); + } + } + } +} 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()); +}