Merge pull request #334 from rouault/enhance_usage_and_help

Several bug fixes in usage, and improvement in usage and help
This commit is contained in:
Pranav 2024-03-13 20:43:43 -04:00 committed by GitHub
commit cebee4bb4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 383 additions and 10 deletions

View File

@ -48,6 +48,7 @@
* [Positional Arguments with Compound Toggle Arguments](#positional-arguments-with-compound-toggle-arguments) * [Positional Arguments with Compound Toggle Arguments](#positional-arguments-with-compound-toggle-arguments)
* [Restricting the set of values for an argument](#restricting-the-set-of-values-for-an-argument) * [Restricting the set of values for an argument](#restricting-the-set-of-values-for-an-argument)
* [Using `option=value` syntax](#using-optionvalue-syntax) * [Using `option=value` syntax](#using-optionvalue-syntax)
* [Advanced usage formatting](#advanced-usage-formatting)
* [Developer Notes](#developer-notes) * [Developer Notes](#developer-notes)
* [Copying and Moving](#copying-and-moving) * [Copying and Moving](#copying-and-moving)
* [CMake Integration](#cmake-integration) * [CMake Integration](#cmake-integration)
@ -1282,6 +1283,68 @@ foo@bar:/home/dev/$ ./test --bar=BAR --foo
--bar: BAR --bar: BAR
``` ```
### Advanced usage formatting
By default usage is reported on a single line.
The ``ArgumentParser::set_usage_max_line_width(width)`` method can be used
to display the usage() on multiple lines, by defining the maximum line width.
It can be combined with a call to ``ArgumentParser::set_usage_break_on_mutex()``
to ask grouped mutually exclusive arguments to be displayed on a separate line.
``ArgumentParser::add_usage_newline()`` can also be used to force the next
argument to be displayed on a new line in the usage output.
The following snippet
```cpp
argparse::ArgumentParser program("program");
program.set_usage_max_line_width(80);
program.set_usage_break_on_mutex();
program.add_argument("--quite-long-option-name").flag();
auto &group = program.add_mutually_exclusive_group();
group.add_argument("-a").flag();
group.add_argument("-b").flag();
program.add_argument("-c").flag();
program.add_argument("--another-one").flag();
program.add_argument("-d").flag();
program.add_argument("--yet-another-long-one").flag();
program.add_argument("--will-go-on-new-line").flag();
program.add_usage_newline();
program.add_argument("--new-line").flag();
std::cout << program.usage() << std::endl;
```
will display:
```console
Usage: program [--help] [--version] [--quite-long-option-name]
[[-a]|[-b]]
[-c] [--another-one] [-d] [--yet-another-long-one]
[--will-go-on-new-line]
[--new-line]
```
Furthermore arguments can be separated into several groups by calling
``ArgumentParser::add_group(group_name)``. Only optional arguments should
be specified after the first call to add_group().
```cpp
argparse::ArgumentParser program("program");
program.set_usage_max_line_width(80);
program.add_argument("-a").flag().help("help_a");
program.add_group("Advanced options");
program.add_argument("-b").flag().help("help_b");
```
will display:
```console
Usage: program [--help] [--version] [-a]
Advanced options:
[-b]
```
## Developer Notes ## Developer Notes
### Copying and Moving ### Copying and Moving

View File

@ -1049,13 +1049,17 @@ public:
const std::string metavar = !m_metavar.empty() ? m_metavar : "VAR"; const std::string metavar = !m_metavar.empty() ? m_metavar : "VAR";
if (m_num_args_range.get_max() > 0) { if (m_num_args_range.get_max() > 0) {
usage << " " << metavar; usage << " " << metavar;
if (m_num_args_range.get_max() > 1) { if (m_num_args_range.get_max() > 1 &&
m_metavar.find("> <") == std::string::npos) {
usage << "..."; usage << "...";
} }
} }
if (!m_is_required) { if (!m_is_required) {
usage << "]"; usage << "]";
} }
if (m_is_repeatable) {
usage << "...";
}
return usage.str(); return usage.str();
} }
@ -1104,6 +1108,11 @@ public:
argument.m_num_args_range == NArgsRange{1, 1}) { argument.m_num_args_range == NArgsRange{1, 1}) {
name_stream << " " << argument.m_metavar; name_stream << " " << argument.m_metavar;
} }
else if (!argument.m_metavar.empty() &&
argument.m_num_args_range.get_min() == argument.m_num_args_range.get_max() &&
argument.m_metavar.find("> <") != std::string::npos) {
name_stream << " " << argument.m_metavar;
}
} }
// align multiline help message // align multiline help message
@ -1142,11 +1151,20 @@ public:
} }
stream << argument.m_num_args_range; stream << argument.m_num_args_range;
bool add_space = false;
if (argument.m_default_value.has_value() && if (argument.m_default_value.has_value() &&
argument.m_num_args_range != NArgsRange{0, 0}) { argument.m_num_args_range != NArgsRange{0, 0}) {
stream << "[default: " << argument.m_default_value_repr << "]"; stream << "[default: " << argument.m_default_value_repr << "]";
add_space = true;
} else if (argument.m_is_required) { } else if (argument.m_is_required) {
stream << "[required]"; stream << "[required]";
add_space = true;
}
if (argument.m_is_repeatable) {
if (add_space) {
stream << " ";
}
stream << "[may be repeated]";
} }
stream << "\n"; stream << "\n";
return stream; return stream;
@ -1486,6 +1504,10 @@ private:
return result; return result;
} }
void set_usage_newline_counter(int i) { m_usage_newline_counter = i; }
void set_group_idx(std::size_t i) { m_group_idx = i; }
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;
@ -1510,6 +1532,8 @@ private:
bool m_is_repeatable : 1; bool m_is_repeatable : 1;
bool m_is_used : 1; bool m_is_used : 1;
std::string_view m_prefix_chars; // ArgumentParser has the prefix_chars std::string_view m_prefix_chars; // ArgumentParser has the prefix_chars
int m_usage_newline_counter = 0;
std::size_t m_group_idx = 0;
}; };
class ArgumentParser { class ArgumentParser {
@ -1585,6 +1609,8 @@ public:
m_positional_arguments.splice(std::cend(m_positional_arguments), m_positional_arguments.splice(std::cend(m_positional_arguments),
m_optional_arguments, argument); m_optional_arguments, argument);
} }
argument->set_usage_newline_counter(m_usage_newline_counter);
argument->set_group_idx(m_group_names.size());
index_argument(argument); index_argument(argument);
return *argument; return *argument;
@ -1613,6 +1639,8 @@ public:
template <typename... Targs> Argument &add_argument(Targs... f_args) { template <typename... Targs> Argument &add_argument(Targs... f_args) {
auto &argument = m_parent.add_argument(std::forward<Targs>(f_args)...); auto &argument = m_parent.add_argument(std::forward<Targs>(f_args)...);
m_elements.push_back(&argument); m_elements.push_back(&argument);
argument.set_usage_newline_counter(m_parent.m_usage_newline_counter);
argument.set_group_idx(m_parent.m_group_names.size());
return argument; return argument;
} }
@ -1646,6 +1674,23 @@ public:
return *this; return *this;
} }
// Ask for the next optional arguments to be displayed on a separate
// line in usage() output. Only effective if set_usage_max_line_width() is
// also used.
ArgumentParser &add_usage_newline() {
++m_usage_newline_counter;
return *this;
}
// Ask for the next optional arguments to be displayed in a separate section
// in usage() and help (<< *this) output.
// For usage(), this is only effective if set_usage_max_line_width() is
// also used.
ArgumentParser &add_group(std::string group_name) {
m_group_names.emplace_back(std::move(group_name));
return *this;
}
ArgumentParser &add_description(std::string description) { ArgumentParser &add_description(std::string description) {
m_description = std::move(description); m_description = std::move(description);
return *this; return *this;
@ -1880,9 +1925,21 @@ public:
} }
for (const auto &argument : parser.m_optional_arguments) { for (const auto &argument : parser.m_optional_arguments) {
if (argument.m_group_idx == 0) {
stream.width(static_cast<std::streamsize>(longest_arg_length)); stream.width(static_cast<std::streamsize>(longest_arg_length));
stream << argument; stream << argument;
} }
}
for (size_t i_group = 0; i_group < parser.m_group_names.size(); ++i_group) {
stream << "\n" << parser.m_group_names[i_group] << " (detailed usage):\n";
for (const auto &argument : parser.m_optional_arguments) {
if (argument.m_group_idx == i_group + 1) {
stream.width(static_cast<std::streamsize>(longest_arg_length));
stream << argument;
}
}
}
bool has_visible_subcommands = std::any_of( bool has_visible_subcommands = std::any_of(
parser.m_subparser_map.begin(), parser.m_subparser_map.end(), parser.m_subparser_map.begin(), parser.m_subparser_map.end(),
@ -1920,24 +1977,141 @@ public:
return out; return out;
} }
// Sets the maximum width for a line of the Usage message
ArgumentParser &set_usage_max_line_width(size_t w) {
this->m_usage_max_line_width = w;
return *this;
}
// Asks to display arguments of mutually exclusive group on separate lines in
// the Usage message
ArgumentParser &set_usage_break_on_mutex() {
this->m_usage_break_on_mutex = true;
return *this;
}
// Format usage part of help only // Format usage part of help only
auto usage() const -> std::string { auto usage() const -> std::string {
std::stringstream stream; std::stringstream stream;
stream << "Usage: " << this->m_program_name; std::string curline("Usage: ");
curline += this->m_program_name;
const bool multiline_usage =
this->m_usage_max_line_width < std::numeric_limits<std::size_t>::max();
const size_t indent_size = curline.size();
const auto deal_with_options_of_group = [&](std::size_t group_idx) {
bool found_options = false;
// Add any options inline here // Add any options inline here
const MutuallyExclusiveGroup *cur_mutex = nullptr;
int usage_newline_counter = -1;
for (const auto &argument : this->m_optional_arguments) { for (const auto &argument : this->m_optional_arguments) {
stream << " " << argument.get_inline_usage(); if (multiline_usage) {
if (argument.m_group_idx != group_idx) {
continue;
}
if (usage_newline_counter != argument.m_usage_newline_counter) {
if (usage_newline_counter >= 0) {
if (curline.size() > indent_size) {
stream << curline << std::endl;
curline = std::string(indent_size, ' ');
}
}
usage_newline_counter = argument.m_usage_newline_counter;
}
}
found_options = true;
const std::string arg_inline_usage = argument.get_inline_usage();
const MutuallyExclusiveGroup *arg_mutex =
get_belonging_mutex(&argument);
if ((cur_mutex != nullptr) && (arg_mutex == nullptr)) {
curline += ']';
if (this->m_usage_break_on_mutex) {
stream << curline << std::endl;
curline = std::string(indent_size, ' ');
}
} else if ((cur_mutex == nullptr) && (arg_mutex != nullptr)) {
if ((this->m_usage_break_on_mutex && curline.size() > indent_size) ||
curline.size() + 3 + arg_inline_usage.size() >
this->m_usage_max_line_width) {
stream << curline << std::endl;
curline = std::string(indent_size, ' ');
}
curline += " [";
} else if ((cur_mutex != nullptr) && (arg_mutex != nullptr)) {
if (cur_mutex != arg_mutex) {
curline += ']';
if (this->m_usage_break_on_mutex ||
curline.size() + 3 + arg_inline_usage.size() >
this->m_usage_max_line_width) {
stream << curline << std::endl;
curline = std::string(indent_size, ' ');
}
curline += " [";
} else {
curline += '|';
}
}
cur_mutex = arg_mutex;
if (curline.size() + 1 + arg_inline_usage.size() >
this->m_usage_max_line_width) {
stream << curline << std::endl;
curline = std::string(indent_size, ' ');
curline += " ";
} else if (cur_mutex == nullptr) {
curline += " ";
}
curline += arg_inline_usage;
}
if (cur_mutex != nullptr) {
curline += ']';
}
return found_options;
};
const bool found_options = deal_with_options_of_group(0);
if (found_options && multiline_usage &&
!this->m_positional_arguments.empty()) {
stream << curline << std::endl;
curline = std::string(indent_size, ' ');
} }
// Put positional arguments after the optionals // Put positional arguments after the optionals
for (const auto &argument : this->m_positional_arguments) { for (const auto &argument : this->m_positional_arguments) {
if (!argument.m_metavar.empty()) { const std::string pos_arg = !argument.m_metavar.empty()
stream << " " << argument.m_metavar; ? argument.m_metavar
: argument.m_names.front();
if (curline.size() + 1 + pos_arg.size() > this->m_usage_max_line_width) {
stream << curline << std::endl;
curline = std::string(indent_size, ' ');
}
curline += " ";
if (argument.m_num_args_range.get_min() == 0 &&
!argument.m_num_args_range.is_right_bounded()) {
curline += "[";
curline += pos_arg;
curline += "]...";
} else if (argument.m_num_args_range.get_min() == 1 &&
!argument.m_num_args_range.is_right_bounded()) {
curline += pos_arg;
curline += "...";
} else { } else {
stream << " " << argument.m_names.front(); curline += pos_arg;
} }
} }
if (multiline_usage) {
// Display options of other groups
for (std::size_t i = 0; i < m_group_names.size(); ++i) {
stream << curline << std::endl << std::endl;
stream << m_group_names[i] << ":" << std::endl;
curline = std::string(indent_size, ' ');
deal_with_options_of_group(i + 1);
}
}
stream << curline;
// Put subcommands after positional arguments // Put subcommands after positional arguments
if (!m_subparser_map.empty()) { if (!m_subparser_map.empty()) {
stream << " {"; stream << " {";
@ -1979,6 +2153,16 @@ public:
void set_suppress(bool suppress) { m_suppress = suppress; } void set_suppress(bool suppress) { m_suppress = suppress; }
protected: protected:
const MutuallyExclusiveGroup *get_belonging_mutex(const Argument *arg) const {
for (const auto &mutex : m_mutually_exclusive_groups) {
if (std::find(mutex.m_elements.begin(), mutex.m_elements.end(), arg) !=
mutex.m_elements.end()) {
return &mutex;
}
}
return nullptr;
}
bool is_valid_prefix_char(char c) const { bool is_valid_prefix_char(char c) const {
return m_prefix_chars.find(c) != std::string::npos; return m_prefix_chars.find(c) != std::string::npos;
} }
@ -2268,6 +2452,10 @@ protected:
std::map<std::string, bool> m_subparser_used; std::map<std::string, bool> m_subparser_used;
std::vector<MutuallyExclusiveGroup> m_mutually_exclusive_groups; std::vector<MutuallyExclusiveGroup> m_mutually_exclusive_groups;
bool m_suppress = false; bool m_suppress = false;
std::size_t m_usage_max_line_width = std::numeric_limits<std::size_t>::max();
bool m_usage_break_on_mutex = false;
int m_usage_newline_counter = 0;
std::vector<std::string> m_group_names;
}; };
} // namespace argparse } // namespace argparse

View File

@ -122,3 +122,125 @@ TEST_CASE("Multiline help message alignment") {
// Make sure we have at least one help message // Make sure we have at least one help message
REQUIRE(help_message_start != -1); REQUIRE(help_message_start != -1);
} }
TEST_CASE("Exclusive arguments, only") {
argparse::ArgumentParser program("program");
auto &group = program.add_mutually_exclusive_group();
group.add_argument("-a").flag();
group.add_argument("-b").flag();
REQUIRE(program.usage() == "Usage: program [--help] [--version] [[-a]|[-b]]");
}
TEST_CASE("Exclusive arguments, several groups") {
argparse::ArgumentParser program("program");
auto &group = program.add_mutually_exclusive_group();
group.add_argument("-a").flag();
group.add_argument("-b").flag();
auto &group2 = program.add_mutually_exclusive_group();
group2.add_argument("-c").flag();
group2.add_argument("-d").flag();
REQUIRE(program.usage() == "Usage: program [--help] [--version] [[-a]|[-b]] [[-c]|[-d]]");
}
TEST_CASE("Exclusive arguments, several groups, in between arg") {
argparse::ArgumentParser program("program");
auto &group = program.add_mutually_exclusive_group();
group.add_argument("-a").flag();
group.add_argument("-b").flag();
program.add_argument("-X").flag();
auto &group2 = program.add_mutually_exclusive_group();
group2.add_argument("-c").flag();
group2.add_argument("-d").flag();
REQUIRE(program.usage() == "Usage: program [--help] [--version] [[-a]|[-b]] [-X] [[-c]|[-d]]");
}
TEST_CASE("Argument repeatable") {
argparse::ArgumentParser program("program");
program.add_argument("-a").flag().append();
REQUIRE(program.usage() == "Usage: program [--help] [--version] [-a]...");
std::ostringstream s;
s << program;
// std::cout << "DEBUG:" << s.str() << std::endl;
REQUIRE(s.str().find(" -a [may be repeated]") != std::string::npos);
}
TEST_CASE("Argument with nargs(2) and metavar <x> <y>") {
argparse::ArgumentParser program("program");
program.add_argument("-foo").metavar("<x> <y>").nargs(2);
REQUIRE(program.usage() == "Usage: program [--help] [--version] [-foo <x> <y>]");
}
TEST_CASE("add_group help") {
argparse::ArgumentParser program("program");
program.add_argument("-a").flag().help("help_a");
program.add_group("Advanced options");
program.add_argument("-b").flag().help("help_b");
REQUIRE(program.usage() == "Usage: program [--help] [--version] [-a] [-b]");
std::ostringstream s;
s << program;
// std::cout << "DEBUG:" << s.str() << std::endl;
REQUIRE(s.str().find(
" -a help_a \n"
"\n"
"Advanced options (detailed usage):\n"
" -b help_b") != std::string::npos);
}
TEST_CASE("multiline usage, several groups") {
argparse::ArgumentParser program("program");
program.set_usage_max_line_width(80);
program.add_argument("-a").flag().help("help_a");
program.add_group("Advanced options");
program.add_argument("-b").flag().help("help_b");
// std::cout << "DEBUG:" << program.usage() << std::endl;
REQUIRE(program.usage() ==
"Usage: program [--help] [--version] [-a]\n"
"\n"
"Advanced options:\n"
" [-b]");
}
TEST_CASE("multiline usage, no break on mutex") {
argparse::ArgumentParser program("program");
program.set_usage_max_line_width(80);
program.set_usage_break_on_mutex();
program.add_argument("--quite-long-option-name").flag();
auto &group = program.add_mutually_exclusive_group();
group.add_argument("-a").flag();
group.add_argument("-b").flag();
program.add_argument("-c").flag();
program.add_argument("--another-one").flag();
program.add_argument("-d").flag();
program.add_argument("--yet-another-long-one").flag();
program.add_argument("--will-go-on-new-line").flag();
// std::cout << "DEBUG:" << program.usage() << std::endl;
REQUIRE(program.usage() ==
"Usage: program [--help] [--version] [--quite-long-option-name]\n"
" [[-a]|[-b]]\n"
" [-c] [--another-one] [-d] [--yet-another-long-one]\n"
" [--will-go-on-new-line]");
}
TEST_CASE("multiline usage, break on mutex") {
argparse::ArgumentParser program("program");
program.set_usage_max_line_width(80);
program.add_argument("--quite-long-option-name").flag();
auto &group = program.add_mutually_exclusive_group();
group.add_argument("-a").flag();
group.add_argument("-b").flag();
program.add_argument("-c").flag();
program.add_argument("--another-one").flag();
program.add_argument("-d").flag();
program.add_argument("--yet-another-long-one").flag();
program.add_argument("--will-go-on-new-line").flag();
program.add_usage_newline();
program.add_argument("--on-a-dedicated-line").flag();
// std::cout << "DEBUG:" << program.usage() << std::endl;
REQUIRE(program.usage() ==
"Usage: program [--help] [--version] [--quite-long-option-name] [[-a]|[-b]] [-c]\n"
" [--another-one] [-d] [--yet-another-long-one]\n"
" [--will-go-on-new-line]\n"
" [--on-a-dedicated-line]");
}