Added support for metavar and improved help/usage based on #187

This commit is contained in:
Pranav Srinivas Kumar 2022-09-21 18:01:36 -07:00
parent 5a83edd3c4
commit 3b9df0b1e7
2 changed files with 200 additions and 52 deletions

View File

@ -338,15 +338,15 @@ The square of 4 is 16
``` ```
foo@bar:/home/dev/$ ./main --help foo@bar:/home/dev/$ ./main --help
Usage: main [options] square Usage: main [-h] [--verbose] square
Positional arguments: Positional arguments:
square display the square of a given number square display the square of a given number
Optional arguments: Optional arguments:
-h --help shows help message and exits [default: false] -h, --help shows help message and exits
-v --version prints version information and exits [default: false] -v, --version prints version information and exits
--verbose [default: false] --verbose
``` ```
You may also get the help message in string via `program.help().str()`. 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. information. `ArgumentParser::add_epilog` will add text after all other help output.
```cpp ```cpp
argparse::ArgumentParser program("main"); #include <argparse/argparse.hpp>
program.add_argument("thing") int main(int argc, char *argv[]) {
.help("Thing to use."); argparse::ArgumentParser program("main");
program.add_description("Forward a thing to the next member."); program.add_argument("thing").help("Thing to use.").metavar("THING");
program.add_epilog("Possible things include betingalw, chiz, and res."); 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 ```console
foo@bar:/home/dev/$ ./main --help Usage: main [-h] [--member ALIAS] [--verbose] THING
Usage: main thing
Forward a thing to the next member. Forward a thing to the next member.
Positional arguments: Positional arguments:
thing Thing to use. THING Thing to use.
Optional arguments: Optional arguments:
-h --help shows help message and exits [default: false] -h, --help shows help message and exits
-v --version prints version information and exits [default: false] -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. Possible things include betingalw, chiz, and res.
``` ```
@ -710,12 +716,14 @@ int main(int argc, char *argv[]) {
// git add subparser // git add subparser
argparse::ArgumentParser add_command("add"); argparse::ArgumentParser add_command("add");
add_command.add_description("Add file contents to the index");
add_command.add_argument("files") add_command.add_argument("files")
.help("Files to add content from. Fileglobs (e.g. *.c) can be given to add all matching files.") .help("Files to add content from. Fileglobs (e.g. *.c) can be given to add all matching files.")
.remaining(); .remaining();
// git commit subparser // git commit subparser
argparse::ArgumentParser commit_command("commit"); argparse::ArgumentParser commit_command("commit");
commit_command.add_description("Record changes to the repository");
commit_command.add_argument("-a", "--all") commit_command.add_argument("-a", "--all")
.help("Tell the command to automatically stage files that have been modified and deleted.") .help("Tell the command to automatically stage files that have been modified and deleted.")
.default_value(false) .default_value(false)
@ -726,6 +734,7 @@ int main(int argc, char *argv[]) {
// git cat-file subparser // git cat-file subparser
argparse::ArgumentParser catfile_command("cat-file"); 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") catfile_command.add_argument("-t")
.help("Instead of the content, show the object type identified by <object>."); .help("Instead of the content, show the object type identified by <object>.");
@ -734,7 +743,9 @@ int main(int argc, char *argv[]) {
// git submodule subparser // git submodule subparser
argparse::ArgumentParser submodule_command("submodule"); argparse::ArgumentParser submodule_command("submodule");
submodule_command.add_description("Initialize, update or inspect submodules");
argparse::ArgumentParser submodule_update_command("update"); 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") submodule_update_command.add_argument("--init")
.default_value(false) .default_value(false)
.implicit_value(true); .implicit_value(true);
@ -763,41 +774,52 @@ int main(int argc, char *argv[]) {
```console ```console
foo@bar:/home/dev/$ ./git --help foo@bar:/home/dev/$ ./git --help
Usage: git [options] <command> [<args>] Usage: git [-h] {add,cat-file,commit,submodule}
Optional arguments: Optional arguments:
-h --help shows help message and exits [default: false] -h, --help shows help message and exits
-v --version prints version information and exits [default: false] -v, --version prints version information and exits
Subcommands: Subcommands:
add Add file contents to the index add Add file contents to the index
cat-file Provide content or type and size information for repository objects cat-file Provide content or type and size information for repository objects
commit Record changes to the repository commit Record changes to the repository
submodule Initialize, update or inspect submodules submodule Initialize, update or inspect submodules
foo@bar:/home/dev/$ ./git add --help foo@bar:/home/dev/$ ./git add --help
Usage: git add [options] files Usage: add [-h] files
Add file contents to the index Add file contents to the index
Positional arguments: 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: Optional arguments:
-h --help shows help message and exits [default: false] -h, --help shows help message and exits
-v --version prints version information and exits [default: false] -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 <msg> as the commit message.
foo@bar:/home/dev/$ ./git submodule --help foo@bar:/home/dev/$ ./git submodule --help
Usage: git submodule [options] <command> [<args>] Usage: submodule [-h] {update}
Initialize, update or inspect submodules Initialize, update or inspect submodules
Optional arguments: Optional arguments:
-h --help shows help message and exits [default: false] -h, --help shows help message and exits
-v --version prints version information and exits [default: false] -v, --version prints version information and exits
Subcommands: 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. 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.

View File

@ -36,6 +36,7 @@ SOFTWARE.
#include <charconv> #include <charconv>
#include <cstdlib> #include <cstdlib>
#include <functional> #include <functional>
#include <iomanip>
#include <iostream> #include <iostream>
#include <iterator> #include <iterator>
#include <limits> #include <limits>
@ -329,6 +330,21 @@ template <class T> struct parse_number<T, chars_format::fixed> {
} }
}; };
template <typename StrIt>
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 } // namespace details
enum class nargs_pattern { optional, any, at_least_one }; enum class nargs_pattern { optional, any, at_least_one };
@ -379,6 +395,11 @@ public:
return *this; return *this;
} }
Argument &metavar(std::string metavar) {
m_metavar = std::move(metavar);
return *this;
}
template <typename T> Argument &default_value(T &&value) { template <typename T> Argument &default_value(T &&value) {
m_default_value_repr = details::repr(value); m_default_value_repr = details::repr(value);
m_default_value = std::forward<T>(value); m_default_value = std::forward<T>(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 { 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) { std::size_t names_size = std::accumulate(
return sum + s.size() + std::begin(m_names), std::end(m_names), std::size_t(0),
1; // +1 for space between names [](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, friend std::ostream &operator<<(std::ostream &stream,
const Argument &argument) { const Argument &argument) {
std::stringstream name_stream; std::stringstream name_stream;
std::copy(std::begin(argument.m_names), std::end(argument.m_names), name_stream << " "; // indent
std::ostream_iterator<std::string>(name_stream, " ")); 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; 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()) { if (!argument.m_help.empty()) {
stream << " "; stream << " ";
} }
@ -647,6 +729,12 @@ private:
std::size_t get_min() const { return m_min; } std::size_t get_min() const { return m_min; }
std::size_t get_max() const { return m_max; } 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 { void throw_nargs_range_validation_error() const {
@ -913,6 +1001,7 @@ private:
std::vector<std::string> m_names; std::vector<std::string> m_names;
std::string_view m_used_name; std::string_view m_used_name;
std::string m_help; std::string m_help;
std::string m_metavar;
std::any m_default_value; std::any m_default_value;
std::string m_default_value_repr; std::string m_default_value_repr;
std::any m_implicit_value; std::any m_implicit_value;
@ -1171,17 +1260,10 @@ public:
friend auto operator<<(std::ostream &stream, const ArgumentParser &parser) friend auto operator<<(std::ostream &stream, const ArgumentParser &parser)
-> std::ostream & { -> std::ostream & {
stream.setf(std::ios_base::left); 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()) { auto longest_arg_length = parser.get_length_of_longest_argument();
stream << (parser.m_positional_arguments.empty() ? "" : " ")
<< "<command> [<args>]"; stream << parser.usage() << "\n\n";
}
std::size_t longest_arg_length = parser.get_length_of_longest_argument();
stream << "\n\n";
if (!parser.m_description.empty()) { if (!parser.m_description.empty()) {
stream << parser.m_description << "\n\n"; stream << parser.m_description << "\n\n";
@ -1212,8 +1294,10 @@ public:
: "\n") : "\n")
<< "Subcommands:\n"; << "Subcommands:\n";
for (const auto &[command, subparser] : parser.m_subparser_map) { for (const auto &[command, subparser] : parser.m_subparser_map) {
stream.width(static_cast<std::streamsize>(longest_arg_length)); stream << std::setw(2) << " ";
stream << command << "\t" << subparser->get().m_description << "\n"; stream << std::setw(static_cast<int>(longest_arg_length - 2))
<< command;
stream << " " << subparser->get().m_description << "\n";
} }
} }
@ -1232,6 +1316,48 @@ public:
return out; 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 // Printing the one and only help message
// I've stuck with a simple message format, nothing fancy. // I've stuck with a simple message format, nothing fancy.
[[deprecated("Use cout << program; instead. See also help().")]] std::string [[deprecated("Use cout << program; instead. See also help().")]] std::string