diff --git a/README.md b/README.md index 40eb9d9..72c6bb8 100644 --- a/README.md +++ b/README.md @@ -338,15 +338,15 @@ The square of 4 is 16 ``` foo@bar:/home/dev/$ ./main --help -Usage: main [options] square +Usage: main [-h] [--verbose] square Positional arguments: -square display the square of a given number + square display the square of a given number Optional arguments: --h --help shows help message and exits [default: false] --v --version prints version information and exits [default: false] ---verbose [default: false] + -h, --help shows help message and exits + -v, --version prints version information and exits + --verbose ``` You may also get the help message in string via `program.help().str()`. @@ -357,30 +357,36 @@ You may also get the help message in string via `program.help().str()`. information. `ArgumentParser::add_epilog` will add text after all other help output. ```cpp -argparse::ArgumentParser program("main"); +#include -program.add_argument("thing") - .help("Thing to use."); -program.add_description("Forward a thing to the next member."); -program.add_epilog("Possible things include betingalw, chiz, and res."); +int main(int argc, char *argv[]) { + argparse::ArgumentParser program("main"); + program.add_argument("thing").help("Thing to use.").metavar("THING"); + program.add_argument("--member").help("The alias for the member to pass to.").metavar("ALIAS"); + program.add_argument("--verbose").default_value(false).implicit_value(true); -program.parse_args(argc, argv); + program.add_description("Forward a thing to the next member."); + program.add_epilog("Possible things include betingalw, chiz, and res."); -std::cout << program << std::endl; + program.parse_args(argc, argv); + + std::cout << program << std::endl; +} ``` ```console -foo@bar:/home/dev/$ ./main --help -Usage: main thing +Usage: main [-h] [--member ALIAS] [--verbose] THING Forward a thing to the next member. Positional arguments: -thing Thing to use. + THING Thing to use. Optional arguments: --h --help shows help message and exits [default: false] --v --version prints version information and exits [default: false] + -h, --help shows help message and exits + -v, --version prints version information and exits + --member ALIAS The alias for the member to pass to. + --verbose Possible things include betingalw, chiz, and res. ``` @@ -710,12 +716,14 @@ int main(int argc, char *argv[]) { // git add subparser argparse::ArgumentParser add_command("add"); + add_command.add_description("Add file contents to the index"); 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_description("Record changes to the repository"); commit_command.add_argument("-a", "--all") .help("Tell the command to automatically stage files that have been modified and deleted.") .default_value(false) @@ -726,6 +734,7 @@ int main(int argc, char *argv[]) { // git cat-file subparser argparse::ArgumentParser catfile_command("cat-file"); + catfile_command.add_description("Provide content or type and size information for repository objects"); catfile_command.add_argument("-t") .help("Instead of the content, show the object type identified by ."); @@ -734,7 +743,9 @@ int main(int argc, char *argv[]) { // git submodule subparser argparse::ArgumentParser submodule_command("submodule"); + submodule_command.add_description("Initialize, update or inspect submodules"); argparse::ArgumentParser submodule_update_command("update"); + submodule_update_command.add_description("Update the registered submodules to match what the superproject expects"); submodule_update_command.add_argument("--init") .default_value(false) .implicit_value(true); @@ -763,41 +774,52 @@ int main(int argc, char *argv[]) { ```console foo@bar:/home/dev/$ ./git --help -Usage: git [options] [] +Usage: git [-h] {add,cat-file,commit,submodule} Optional arguments: --h --help shows help message and exits [default: false] --v --version prints version information and exits [default: false] + -h, --help shows help message and exits + -v, --version prints version information and exits Subcommands: -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 + 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 foo@bar:/home/dev/$ ./git add --help -Usage: git add [options] files +Usage: add [-h] 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. + files Files to add content from. Fileglobs (e.g. *.c) can be given to add all matching files. Optional arguments: --h --help shows help message and exits [default: false] --v --version prints version information and exits [default: false] + -h, --help shows help message and exits + -v, --version prints version information and exits + +foo@bar:/home/dev/$ ./git commit --help +Usage: commit [-h] [--all] [--message VAR] + +Record changes to the repository + +Optional arguments: + -h, --help shows help message and exits + -v, --version prints version information and exits + -a, --all Tell the command to automatically stage files that have been modified and deleted. + -m, --message Use the given as the commit message. foo@bar:/home/dev/$ ./git submodule --help -Usage: git submodule [options] [] +Usage: submodule [-h] {update} 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] + -h, --help shows help message and exits + -v, --version prints version information and exits Subcommands: -update Update the registered submodules to match what the superproject expects + 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. diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index ba27b43..87bbfe0 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -36,6 +36,7 @@ SOFTWARE. #include #include #include +#include #include #include #include @@ -329,6 +330,21 @@ template struct parse_number { } }; +template +std::string join(StrIt first, StrIt last, const std::string &separator) { + if (first == last) { + return ""; + } + std::stringstream value; + value << *first; + ++first; + while (first != last) { + value << separator << *first; + ++first; + } + return value.str(); +} + } // namespace details enum class nargs_pattern { optional, any, at_least_one }; @@ -379,6 +395,11 @@ public: return *this; } + Argument &metavar(std::string metavar) { + m_metavar = std::move(metavar); + return *this; + } + template Argument &default_value(T &&value) { m_default_value_repr = details::repr(value); m_default_value = std::forward(value); @@ -573,21 +594,82 @@ public: } } + std::string get_inline_usage() const { + std::stringstream usage; + // Find the longest variant to show in the usage string + std::string longest_name = m_names[0]; + for (const auto &s : m_names) { + if (s.size() > longest_name.size()) { + longest_name = s; + } + } + if (!m_is_required) { + usage << "["; + } + usage << longest_name; + const std::string metavar = !m_metavar.empty() ? m_metavar : "VAR"; + if (m_num_args_range.get_max() > 0) { + usage << " " << metavar; + if (m_num_args_range.get_max() > 1) { + usage << "..."; + } + } + if (!m_is_required) { + usage << "]"; + } + return usage.str(); + } + std::size_t get_arguments_length() const { - return std::accumulate(std::begin(m_names), std::end(m_names), - std::size_t(0), [](const auto &sum, const auto &s) { - return sum + s.size() + - 1; // +1 for space between names - }); + + std::size_t names_size = std::accumulate( + std::begin(m_names), std::end(m_names), std::size_t(0), + [](const auto &sum, const auto &s) { return sum + s.size(); }); + + if (is_positional(m_names.front(), m_prefix_chars)) { + // A set metavar means this replaces the names + if (!m_metavar.empty()) { + // Indent and metavar + return 2 + m_metavar.size(); + } + + // Indent and space-separated + return 2 + names_size + (m_names.size() - 1); + } + // Is an option - include both names _and_ metavar + // size = text + (", " between names) + std::size_t size = names_size + 2 * (m_names.size() - 1); + if (!m_metavar.empty() && m_num_args_range == NArgsRange{1, 1}) { + size += m_metavar.size() + 1; + } + return size + 2; // indent } friend std::ostream &operator<<(std::ostream &stream, const Argument &argument) { std::stringstream name_stream; - std::copy(std::begin(argument.m_names), std::end(argument.m_names), - std::ostream_iterator(name_stream, " ")); + name_stream << " "; // indent + if (argument.is_positional(argument.m_names.front(), + argument.m_prefix_chars)) { + if (!argument.m_metavar.empty()) { + name_stream << argument.m_metavar; + } else { + name_stream << details::join(argument.m_names.begin(), + argument.m_names.end(), " "); + } + } else { + name_stream << details::join(argument.m_names.begin(), + argument.m_names.end(), ", "); + // If we have a metavar, and one narg - print the metavar + if (!argument.m_metavar.empty() && + argument.m_num_args_range == NArgsRange{1, 1}) { + name_stream << " " << argument.m_metavar; + } + } stream << name_stream.str() << "\t" << argument.m_help; - if (argument.m_default_value.has_value()) { + + if (argument.m_default_value.has_value() && + argument.m_num_args_range != NArgsRange{0, 0}) { if (!argument.m_help.empty()) { stream << " "; } @@ -647,6 +729,12 @@ private: std::size_t get_min() const { return m_min; } std::size_t get_max() const { return m_max; } + + bool operator==(const NArgsRange &rhs) const { + return rhs.m_min == m_min && rhs.m_max == m_max; + } + + bool operator!=(const NArgsRange &rhs) const { return !(*this == rhs); } }; void throw_nargs_range_validation_error() const { @@ -913,6 +1001,7 @@ private: std::vector m_names; std::string_view m_used_name; std::string m_help; + std::string m_metavar; std::any m_default_value; std::string m_default_value_repr; std::any m_implicit_value; @@ -1171,17 +1260,10 @@ public: friend auto operator<<(std::ostream &stream, const ArgumentParser &parser) -> std::ostream & { stream.setf(std::ios_base::left); - stream << "Usage: " << parser.m_parser_path << " [options] "; - for (const auto &argument : parser.m_positional_arguments) { - stream << argument.m_names.front() << " "; - } - if (!parser.m_subparser_map.empty()) { - stream << (parser.m_positional_arguments.empty() ? "" : " ") - << " []"; - } - std::size_t longest_arg_length = parser.get_length_of_longest_argument(); - stream << "\n\n"; + auto longest_arg_length = parser.get_length_of_longest_argument(); + + stream << parser.usage() << "\n\n"; if (!parser.m_description.empty()) { stream << parser.m_description << "\n\n"; @@ -1212,8 +1294,10 @@ public: : "\n") << "Subcommands:\n"; for (const auto &[command, subparser] : parser.m_subparser_map) { - stream.width(static_cast(longest_arg_length)); - stream << command << "\t" << subparser->get().m_description << "\n"; + stream << std::setw(2) << " "; + stream << std::setw(static_cast(longest_arg_length - 2)) + << command; + stream << " " << subparser->get().m_description << "\n"; } } @@ -1232,6 +1316,48 @@ public: return out; } + // Format usage part of help only + auto usage() const -> std::string { + std::stringstream stream; + + stream << "Usage: " << this->m_program_name; + + // Add any options inline here + for (const auto &argument : this->m_optional_arguments) { + if (argument.m_names[0] == "-v") { + continue; + } else if (argument.m_names[0] == "-h") { + stream << " [-h]"; + } else { + stream << " " << argument.get_inline_usage(); + } + } + // Put positional arguments after the optionals + for (const auto &argument : this->m_positional_arguments) { + if (!argument.m_metavar.empty()) { + stream << " " << argument.m_metavar; + } else { + stream << " " << argument.m_names.front(); + } + } + // Put subcommands after positional arguments + if (!m_subparser_map.empty()) { + stream << " {"; + std::size_t i{0}; + for (const auto &[command, unused] : m_subparser_map) { + if (i == 0) { + stream << command; + } else { + stream << "," << command; + } + ++i; + } + stream << "}"; + } + + return stream.str(); + } + // Printing the one and only help message // I've stuck with a simple message format, nothing fancy. [[deprecated("Use cout << program; instead. See also help().")]] std::string