From 79e55658c1192a70a16e412f4b1830ca71ef3e7f Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Wed, 13 Mar 2024 01:48:59 +0100 Subject: [PATCH] Several bug fixes in usage, and improvement in usage and help - Display mutually exclusive arguments as ``[[-a]|[-b]]`` in usage - Add ... trailer to repeatable arguments in usage: ``[-x]...`` - Implement the following enhancements: 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 line, by defining the maximum line width. It can be accompanied by a call to ``ArgumentParser::set_usage_break_on_mutex()`` to ask grouped mutually exclusive arguments to be displayed on a separate line. 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(); 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] ``` 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] ``` --- README.md | 57 ++++++++++ include/argparse/argparse.hpp | 208 ++++++++++++++++++++++++++++++++-- test/test_help.cpp | 119 +++++++++++++++++++ 3 files changed, 374 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index bf22610a..8c84d5b9 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ * [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) * [Using `option=value` syntax](#using-optionvalue-syntax) + * [Advanced usage formatting](#advanced-usage-formatting) * [Developer Notes](#developer-notes) * [Copying and Moving](#copying-and-moving) * [CMake Integration](#cmake-integration) @@ -1282,6 +1283,62 @@ foo@bar:/home/dev/$ ./test --bar=BAR --foo --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 line, by defining the maximum line width. + +It can be accompanied by a call to ``ArgumentParser::set_usage_break_on_mutex()`` +to ask grouped mutually exclusive arguments to be displayed on a separate line. + +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(); + 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] +``` + +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 ### Copying and Moving diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index 1fe67b15..e1718241 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -1049,13 +1049,17 @@ class Argument { 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) { + if (m_num_args_range.get_max() > 1 && + m_metavar.find("> <") == std::string::npos) { usage << "..."; } } if (!m_is_required) { usage << "]"; } + if (m_is_repeatable) { + usage << "..."; + } return usage.str(); } @@ -1104,6 +1108,11 @@ class Argument { argument.m_num_args_range == NArgsRange{1, 1}) { 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 @@ -1142,11 +1151,20 @@ class Argument { } stream << argument.m_num_args_range; + bool add_space = false; if (argument.m_default_value.has_value() && argument.m_num_args_range != NArgsRange{0, 0}) { stream << "[default: " << argument.m_default_value_repr << "]"; + add_space = true; } else if (argument.m_is_required) { stream << "[required]"; + add_space = true; + } + if (argument.m_is_repeatable) { + if (add_space) { + stream << " "; + } + stream << "[may be repeated]"; } stream << "\n"; return stream; @@ -1486,6 +1504,10 @@ class Argument { 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 m_names; std::string_view m_used_name; std::string m_help; @@ -1510,6 +1532,8 @@ class Argument { bool m_is_repeatable : 1; bool m_is_used : 1; 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 { @@ -1585,6 +1609,8 @@ class ArgumentParser { m_positional_arguments.splice(std::cend(m_positional_arguments), m_optional_arguments, argument); } + argument->set_usage_newline_counter(m_usage_newline_counter); + argument->set_group_idx(m_group_names.size()); index_argument(argument); return *argument; @@ -1613,6 +1639,8 @@ class ArgumentParser { template Argument &add_argument(Targs... f_args) { auto &argument = m_parent.add_argument(std::forward(f_args)...); 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; } @@ -1646,6 +1674,23 @@ class ArgumentParser { 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) { m_description = std::move(description); return *this; @@ -1880,8 +1925,20 @@ class ArgumentParser { } for (const auto &argument : parser.m_optional_arguments) { - stream.width(static_cast(longest_arg_length)); - stream << argument; + if (argument.m_group_idx == 0) { + stream.width(static_cast(longest_arg_length)); + 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(longest_arg_length)); + stream << argument; + } + } } bool has_visible_subcommands = std::any_of( @@ -1920,24 +1977,141 @@ class ArgumentParser { 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 auto usage() const -> std::string { 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::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 + const MutuallyExclusiveGroup *cur_mutex = nullptr; + int usage_newline_counter = -1; + for (const auto &argument : this->m_optional_arguments) { + 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); - // Add any options inline here - for (const auto &argument : this->m_optional_arguments) { - stream << " " << argument.get_inline_usage(); + 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 for (const auto &argument : this->m_positional_arguments) { - if (!argument.m_metavar.empty()) { - stream << " " << argument.m_metavar; + const std::string pos_arg = !argument.m_metavar.empty() + ? 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 { - 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 if (!m_subparser_map.empty()) { stream << " {"; @@ -1979,6 +2153,16 @@ class ArgumentParser { void set_suppress(bool suppress) { m_suppress = suppress; } 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 { return m_prefix_chars.find(c) != std::string::npos; } @@ -2268,6 +2452,10 @@ class ArgumentParser { std::map m_subparser_used; std::vector m_mutually_exclusive_groups; bool m_suppress = false; + std::size_t m_usage_max_line_width = std::numeric_limits::max(); + bool m_usage_break_on_mutex = false; + int m_usage_newline_counter = 0; + std::vector m_group_names; }; } // namespace argparse diff --git a/test/test_help.cpp b/test/test_help.cpp index 864a8077..aabce00a 100644 --- a/test/test_help.cpp +++ b/test/test_help.cpp @@ -122,3 +122,122 @@ TEST_CASE("Multiline help message alignment") { // Make sure we have at least one help message 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 ") { + argparse::ArgumentParser program("program"); + program.add_argument("-foo").metavar(" ").nargs(2); + REQUIRE(program.usage() == "Usage: program [--help] [--version] [-foo ]"); +} + +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(); + // 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]"); +}