diff --git a/README.md b/README.md index bfb3289..ecac5d1 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,8 @@ int main(int argc, char *argv[]) { And running the code: -```bash -$ ./main 15 +```console +foo@bar:/home/dev/$ ./main 15 225 ``` @@ -141,8 +141,8 @@ if (program["--verbose"] == true) { } ``` -```bash -$ ./main --verbose +```console +foo@bar:/home/dev/$ ./main --verbose Verbosity enabled ``` @@ -276,8 +276,8 @@ catch (const std::runtime_error& err) { // Some code to print arguments ``` -```bash -$ ./main -5 -1.1 -3.1415 -3.1e2 -4.51329E3 +```console +foo@bar:/home/dev/$ ./main -5 -1.1 -3.1415 -3.1e2 -4.51329E3 integer : -5 floats : -1.1 -3.1415 -310 -4513.29 ``` @@ -316,14 +316,14 @@ else { } ``` -```bash -$ ./main 4 +```console +foo@bar:/home/dev/$ ./main 4 16 -$ ./main 4 --verbose +foo@bar:/home/dev/$ ./main 4 --verbose The square of 4 is 16 -$ ./main --verbose 4 +foo@bar:/home/dev/$ ./main --verbose 4 The square of 4 is 16 ``` @@ -332,7 +332,7 @@ The square of 4 is 16 `std::cout << program` prints a help message, including the program usage and information about the arguments registered with the `ArgumentParser`. For the previous example, here's the default help message: ``` -$ ./main --help +foo@bar:/home/dev/$ ./main --help Usage: main [options] square Positional arguments: @@ -364,8 +364,8 @@ program.parse_args(argc, argv); std::cout << program << std::endl; ``` -```bash -$ ./main --help +```console +foo@bar:/home/dev/$ ./main --help Usage: main thing Forward a thing to the next member. @@ -491,13 +491,13 @@ auto c = program.get>("-c"); // {1.95, 2.47} /// Some code that prints parsed arguments ``` -```bash -$ ./main -ac 3.14 2.718 +```console +foo@bar:/home/dev/$ ./main -ac 3.14 2.718 a = true b = false c = {3.14, 2.718} -$ ./main -cb +foo@bar:/home/dev/$ ./main -cb a = false b = true c = {0.0, 0.0} @@ -568,8 +568,8 @@ The default is `default_arguments::all` for included arguments. No default argum `argparse` supports gathering "remaining" arguments at the end of the command, e.g., for use in a compiler: -```bash -$ compiler file1 file2 file3 +```console +foo@bar:/home/dev/$ compiler file1 file2 file3 ``` To enable this, simply create an argument and mark it as `remaining`. All remaining arguments passed to argparse are gathered here. @@ -601,15 +601,15 @@ try { When no arguments are provided: -```bash -$ ./compiler +```console +foo@bar:/home/dev/$ ./compiler No files provided ``` and when multiple arguments are provided: -```bash -$ ./compiler foo.txt bar.txt baz.txt +```console +foo@bar:/home/dev/$ ./compiler foo.txt bar.txt baz.txt 3 files provided foo.txt bar.txt @@ -650,8 +650,8 @@ try { ``` -```bash -$ ./compiler -o main foo.cpp bar.cpp baz.cpp +```console +foo@bar:/home/dev/$ ./compiler -o main foo.cpp bar.cpp baz.cpp Output filename: main 3 files provided foo.cpp @@ -661,8 +661,8 @@ baz.cpp ***NOTE***: Remember to place all optional arguments BEFORE the remaining argument. If the optional argument is placed after the remaining arguments, it too will be deemed remaining: -```bash -$ ./compiler foo.cpp bar.cpp baz.cpp -o main +```console +foo@bar:/home/dev/$ ./compiler foo.cpp bar.cpp baz.cpp -o main 5 arguments provided foo.cpp bar.cpp @@ -703,11 +703,13 @@ Many programs split up their functionality into a number of sub-commands, for ex int main(int argc, char *argv[]) { argparse::ArgumentParser program("git"); + // git add subparser argparse::ArgumentParser add_command("add"); add_command.add_argument("files") .help("Files to add content from. Fileglobs (e.g. *.c) can be given to add all matching files.") .remaining(); + // git commit subparser argparse::ArgumentParser commit_command("commit"); commit_command.add_argument("-a", "--all") .help("Tell the command to automatically stage files that have been modified and deleted.") @@ -717,6 +719,7 @@ int main(int argc, char *argv[]) { commit_command.add_argument("-m", "--message") .help("Use the given as the commit message."); + // git cat-file subparser argparse::ArgumentParser catfile_command("cat-file"); catfile_command.add_argument("-t") .help("Instead of the content, show the object type identified by ."); @@ -724,6 +727,7 @@ int main(int argc, char *argv[]) { catfile_command.add_argument("-p") .help("Pretty-print the contents of based on its type."); + // git submodule subparser argparse::ArgumentParser submodule_command("submodule"); argparse::ArgumentParser submodule_update_command("update"); submodule_update_command.add_argument("--init") @@ -752,8 +756,8 @@ int main(int argc, char *argv[]) { } ``` -```bash -$ ./git --help +```console +foo@bar:/home/dev/$ ./git --help Usage: git [options] [] Optional arguments: @@ -761,11 +765,16 @@ Optional arguments: -v --version prints version information and exits [default: false] Subcommands: -{add, cat-file, commit, submodule} +add Add file contents to the index +cat-file Provide content or type and size information for repository objects +commit Record changes to the repository +submodule Initialize, update or inspect submodules -$ ./git add --help +foo@bar:/home/dev/$ ./git add --help Usage: git add [options] files +Add file contents to the index + Positional arguments: files Files to add content from. Fileglobs (e.g. *.c) can be given to add all matching files. @@ -773,17 +782,23 @@ Optional arguments: -h --help shows help message and exits [default: false] -v --version prints version information and exits [default: false] -$ ./git submodule update --help -Usage: submodule update [options] +foo@bar:/home/dev/$ ./git submodule --help +Usage: git submodule [options] [] + +Initialize, update or inspect submodules Optional arguments: -h --help shows help message and exits [default: false] -v --version prints version information and exits [default: false] ---init [default: false] ---recursive [default: false] +Subcommands: +update Update the registered submodules to match what the superproject expects ``` +When a help message is requested from a subparser, only the help for that particular parser will be printed. The help message will not include parent parser or sibling parser messages. + +Additionally, every parser has a `.is_subcommand_used("")` member function to check if a subcommand was used. + ## Further Examples ### Construct a JSON object from a filename argument @@ -854,8 +869,8 @@ auto files = program.get>("--files"); // {"a.txt", "b. /// Some code that prints parsed arguments ``` -```bash -$ ./main 1 2 3 -abc 3.14 2.718 --files a.txt b.txt c.txt +```console +foo@bar:/home/dev/$ ./main 1 2 3 -abc 3.14 2.718 --files a.txt b.txt c.txt numbers = {1, 2, 3} a = true b = true @@ -891,8 +906,8 @@ auto input = program.get("input"); std::cout << input << std::endl; ``` -```bash -$ ./main fex +```console +foo@bar:/home/dev/$ ./main fex baz ``` diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index fb3ad9f..24c68cc 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -925,7 +925,7 @@ public: std::string version = "1.0", default_arguments add_args = default_arguments::all) : m_program_name(std::move(program_name)), m_version(std::move(version)), - m_parser_path(m_program_name) { + m_parser_path(m_program_name) { if ((add_args & default_arguments::help) == default_arguments::help) { add_argument("-h", "--help") .action([&](const auto & /*unused*/) { @@ -959,8 +959,7 @@ public: m_is_parsed(other.m_is_parsed), m_positional_arguments(other.m_positional_arguments), m_optional_arguments(other.m_optional_arguments), - m_parser_path(other.m_parser_path), - m_subparsers(other.m_subparsers) { + m_parser_path(other.m_parser_path), m_subparsers(other.m_subparsers) { for (auto it = std::begin(m_positional_arguments); it != std::end(m_positional_arguments); ++it) { index_argument(it); @@ -969,9 +968,10 @@ public: it != std::end(m_optional_arguments); ++it) { index_argument(it); } - for (auto it = std::begin(m_subparsers); - it != std::end(m_subparsers); ++it) { + for (auto it = std::begin(m_subparsers); it != std::end(m_subparsers); + ++it) { m_subparser_map.insert_or_assign(it->get().m_program_name, it); + m_subparser_used.insert_or_assign(it->get().m_program_name, false); } } @@ -1082,6 +1082,13 @@ public: return (*this)[arg_name].m_is_used; } + /* Getter that returns true for user-supplied options. Returns false if not + * user-supplied, even with a default value. + */ + auto is_subcommand_used(std::string_view subcommand_name) const { + return m_subparser_used.at(subcommand_name); + } + /* Indexing operator. Return a reference to an Argument object * Used in conjuction with Argument.operator== e.g., parser["foo"] == true * @throws std::logic_error in case of an invalid argument name @@ -1153,17 +1160,10 @@ public: ? (parser.m_optional_arguments.empty() ? "" : "\n") : "\n") << "Subcommands:\n"; - stream << "{"; - std::size_t i = 0; - for (const auto &[argument, unused] : parser.m_subparser_map) { - if (i == 0) { - stream << argument; - } else { - stream << ", " << argument; - } - ++i; + for (const auto &[command, subparser] : parser.m_subparser_map) { + stream.width(longest_arg_length); + stream << command << "\t" << subparser->get().m_description << "\n"; } - stream << "}\n"; } if (!parser.m_epilog.empty()) { @@ -1194,6 +1194,7 @@ public: parser.m_parser_path = m_program_name + " " + parser.m_program_name; auto it = m_subparsers.emplace(std::cend(m_subparsers), parser); m_subparser_map.insert_or_assign(parser.m_program_name, it); + m_subparser_used.insert_or_assign(parser.m_program_name, false); } private: @@ -1223,6 +1224,7 @@ private: // invoke subparser m_is_parsed = true; + m_subparser_used[maybe_command] = true; return subparser_it->second->get().parse_args( unprocessed_arguments); } @@ -1296,6 +1298,7 @@ private: std::string m_parser_path; std::list> m_subparsers; std::map> m_subparser_map; + std::map> m_subparser_used; }; } // namespace argparse diff --git a/test/test_subparsers.cpp b/test/test_subparsers.cpp index c40ec1d..e4a4274 100644 --- a/test/test_subparsers.cpp +++ b/test/test_subparsers.cpp @@ -17,6 +17,7 @@ TEST_CASE("Add subparsers" * test_suite("subparsers")) { program.add_subparser(command_2); program.parse_args({"test", "--output", "thrust_profile.csv"}); + REQUIRE(program.is_subcommand_used("add") == false); REQUIRE(program.get("--output") == "thrust_profile.csv"); } @@ -37,6 +38,7 @@ TEST_CASE("Parse subparser command" * test_suite("subparsers")) { SUBCASE("command 1") { program.parse_args({"test", "add", "file1.txt", "file2.txt"}); + REQUIRE(program.is_subcommand_used("add") == true); REQUIRE(command_1.is_used("file")); REQUIRE((command_1.get>("file") == std::vector{"file1.txt", "file2.txt"})); @@ -44,6 +46,7 @@ TEST_CASE("Parse subparser command" * test_suite("subparsers")) { SUBCASE("command 2") { program.parse_args({"test", "clean", "--fullclean"}); + REQUIRE(program.is_subcommand_used("clean") == true); REQUIRE(command_2.get("--fullclean") == true); } } @@ -66,6 +69,7 @@ TEST_CASE("Parse subparser command with optional argument" * SUBCASE("Optional argument BEFORE subcommand") { program.parse_args({"test", "--verbose", "clean", "--fullclean"}); + REQUIRE(program.is_subcommand_used("clean") == true); REQUIRE(program.get("--verbose") == true); REQUIRE(command_2.get("--fullclean") == true); } @@ -98,6 +102,7 @@ TEST_CASE("Parse subparser command with parent parser" * SUBCASE("Optional argument BEFORE subcommand") { program.parse_args({"test", "--verbose", "clean", "--fullclean"}); + REQUIRE(program.is_subcommand_used("clean") == true); REQUIRE(program.get("--verbose") == true); REQUIRE(command_2.get("--fullclean") == true); } @@ -141,12 +146,14 @@ TEST_CASE("Parse git commands" * test_suite("subparsers")) { SUBCASE("git add") { program.parse_args({"git", "add", "main.cpp", "foo.hpp", "foo.cpp"}); + REQUIRE(program.is_subcommand_used("add") == true); REQUIRE((add_command.get>("files") == std::vector{"main.cpp", "foo.hpp", "foo.cpp"})); } SUBCASE("git commit") { program.parse_args({"git", "commit", "-am", "Initial commit"}); + REQUIRE(program.is_subcommand_used("commit") == true); REQUIRE(commit_command.get("-a") == true); REQUIRE(commit_command.get("-m") == std::string{"Initial commit"}); @@ -154,31 +161,41 @@ TEST_CASE("Parse git commands" * test_suite("subparsers")) { SUBCASE("git cat-file -t") { program.parse_args({"git", "cat-file", "-t", "3739f5"}); + REQUIRE(program.is_subcommand_used("cat-file") == true); REQUIRE(catfile_command.get("-t") == std::string{"3739f5"}); } SUBCASE("git cat-file -p") { program.parse_args({"git", "cat-file", "-p", "3739f5"}); + REQUIRE(program.is_subcommand_used("cat-file") == true); REQUIRE(catfile_command.get("-p") == std::string{"3739f5"}); } SUBCASE("git submodule update") { program.parse_args({"git", "submodule", "update"}); + REQUIRE(program.is_subcommand_used("submodule") == true); + REQUIRE(submodule_command.is_subcommand_used("update") == true); } SUBCASE("git submodule update --init") { program.parse_args({"git", "submodule", "update", "--init"}); + REQUIRE(program.is_subcommand_used("submodule") == true); + REQUIRE(submodule_command.is_subcommand_used("update") == true); REQUIRE(submodule_update_command.get("--init") == true); REQUIRE(submodule_update_command.get("--recursive") == false); } SUBCASE("git submodule update --recursive") { program.parse_args({"git", "submodule", "update", "--recursive"}); + REQUIRE(program.is_subcommand_used("submodule") == true); + REQUIRE(submodule_command.is_subcommand_used("update") == true); REQUIRE(submodule_update_command.get("--recursive") == true); } SUBCASE("git submodule update --init --recursive") { program.parse_args({"git", "submodule", "update", "--init", "--recursive"}); + REQUIRE(program.is_subcommand_used("submodule") == true); + REQUIRE(submodule_command.is_subcommand_used("update") == true); REQUIRE(submodule_update_command.get("--init") == true); REQUIRE(submodule_update_command.get("--recursive") == true); }