Skip to content

Commit

Permalink
Several bug fixes in usage, and improvement in usage and help
Browse files Browse the repository at this point in the history
- 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 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]
```
  • Loading branch information
rouault committed Mar 13, 2024
1 parent 0fa7d59 commit 8f5b3e0
Show file tree
Hide file tree
Showing 3 changed files with 383 additions and 10 deletions.
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -1282,6 +1283,68 @@ 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 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

### Copying and Moving
Expand Down
208 changes: 198 additions & 10 deletions include/argparse/argparse.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<std::string> m_names;
std::string_view m_used_name;
std::string m_help;
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1613,6 +1639,8 @@ class ArgumentParser {
template <typename... Targs> Argument &add_argument(Targs... f_args) {
auto &argument = m_parent.add_argument(std::forward<Targs>(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;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1880,8 +1925,20 @@ class ArgumentParser {
}

for (const auto &argument : parser.m_optional_arguments) {
stream.width(static_cast<std::streamsize>(longest_arg_length));
stream << argument;
if (argument.m_group_idx == 0) {
stream.width(static_cast<std::streamsize>(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<std::streamsize>(longest_arg_length));
stream << argument;
}
}
}

bool has_visible_subcommands = std::any_of(
Expand Down Expand Up @@ -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<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
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 << " {";
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -2268,6 +2452,10 @@ class ArgumentParser {
std::map<std::string, bool> m_subparser_used;
std::vector<MutuallyExclusiveGroup> m_mutually_exclusive_groups;
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
Loading

0 comments on commit 8f5b3e0

Please sign in to comment.