option name formatting in help (#1247)

Add some controls to manipulate option string formatting, including
disabling the default values, disabling default flag values, disabling
type names.

Fixes #857

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Philip Top
2025-11-03 04:55:14 -08:00
committed by GitHub
parent 3a1946e965
commit 3a69ed51c0
9 changed files with 241 additions and 48 deletions

View File

@@ -5,7 +5,7 @@ on:
permissions:
contents: write
pull-request: write
pull-requests: write
jobs:
label-merged:

View File

@@ -38,8 +38,8 @@ set with a simple and intuitive interface.
- [Option options](#option-options)
- [Validators](#validators)
- [Default Validators](#default-validators)
- [Validators that may be disabled 🚧](#validators-that-may-be-disabled-)
- [Extra Validators 🚧](#extra-validators-)
- [Validators that may be disabled 🆕](#validators-that-may-be-disabled-)
- [Extra Validators 🆕](#extra-validators-)
- [Validator Usage](#validator-usage)
- [Transforming Validators](#transforming-validators)
- [Validator operations](#validator-operations)
@@ -166,8 +166,8 @@ this library:
incomplete arguments. It's better not to guess. Most third party command line
parsers for python actually reimplement command line parsing rather than using
argparse because of this perceived design flaw (recent versions do have an
option to disable it). 🆕 The latest version of CLI11 does include partial
option matching for option prefixes. This is enabled by
option to disable it). Recent releases of CLI11 do include partial option
matching for option prefixes 🆕. This is enabled by
`.allow_subcommand_prefix_matching()`, along with an example that generates
suggested close matches.
- Autocomplete: This might eventually be added to both Plumbum and CLI11, but it
@@ -599,7 +599,7 @@ the two function are that checks do not modify the input whereas transforms can
and are executed before any Validators added through `check`.
CLI11 has several Validators included that perform some common checks. By
default the most commonly used ones are available. 🚧 If some are not needed
default the most commonly used ones are available. 🆕 If some are not needed
they can be disabled by using
```c++
@@ -625,7 +625,7 @@ of flags.
- `CLI::NonNegativeNumber`: Requires the number be greater or equal to 0
- `CLI::Number`: Requires the input be a number.
#### Validators that may be disabled 🚧
#### Validators that may be disabled 🆕
Validators that may be disabled by setting `CLI11_DISABLE_EXTRA_VALIDATORS` to 1
or enabled by setting `CLI11_ENABLE_EXTRA_VALIDATORS` to 1. By default they are
@@ -660,7 +660,7 @@ computation time that may not be valuable for some use cases.
the input be convertible to an `unsigned int` regardless of the end
conversion.
#### Extra Validators 🚧
#### Extra Validators 🆕
New validators will go into code sections that must be explicitly enabled by
setting `CLI11_ENABLE_EXTRA_VALIDATORS` to 1
@@ -842,7 +842,7 @@ It is also possible to create a subclass of `CLI::Validator`, in which case it
can also set a custom description function, and operation function. One example
of this is in the
[custom validator example](https://github.com/CLIUtils/CLI11/blob/main/examples/custom_validator.cpp).
example. The `check` and `transform` operations can also take a shared_ptr 🚧 to
example. The `check` and `transform` operations can also take a shared_ptr 🆕 to
a validator if you wish to reuse the validator in multiple locations or it is
mutating and the check is dependent on other operations or is variable. Note
that in this case it is not recommended to use the same object for both check
@@ -1076,9 +1076,14 @@ option_groups. These are:
for processing the app for custom output formats).
- `.parse_order()`: Get the list of option pointers in the order they were
parsed (including duplicates).
- `.formatter(fmt)`: Set a formatter, with signature
`std::string(const App*, std::string, AppFormatMode)`. See Formatting for more
details.
- `.formatter(std::shared_ptr<formatterBase> fmt)`: Set a custom formatter for
help.
- `.formatter_fn(fmt)`, with signature
`std::string(const App*, std::string, AppFormatMode)`. See [formatting][] for
more details.
- `.config_formatter(std::shared_ptr<Config> fmt)`: set a custom config
formatter for generating config files, more details available at [Config
files][config]
- `.description(str)`: Set/change the description.
- `.get_description()`: Access the description.
- `.alias(str)`: set an alias for the subcommand, this allows subcommands to be
@@ -1451,25 +1456,18 @@ The default settings for options are inherited to subcommands, as well.
### Formatting
The job of formatting help printouts is delegated to a formatter callable object
on Apps and Options. You are free to replace either formatter by calling
`formatter(fmt)` on an `App`, where fmt is any copyable callable with the
correct signature. CLI11 comes with a default App formatter functional,
`Formatter`. It is customizable; you can set `label(key, value)` to replace the
default labels like `REQUIRED`, and `column_width(n)` to set the width of the
columns before you add the functional to the app or option. You can also
override almost any stage of the formatting process in a subclass of either
formatter. If you want to make a new formatter from scratch, you can do that
too; you just need to implement the correct signature. The first argument is a
const pointer to the in question. The formatter will get a `std::string` usage
name as the second option, and a `AppFormatMode` mode for the final option. It
should return a `std::string`.
The `AppFormatMode` can be `Normal`, `All`, or `Sub`, and it indicates the
situation the help was called in. `Sub` is optional, but the default formatter
uses it to make sure expanded subcommands are called with their own formatter
since you can't access anything but the call operator once a formatter has been
set.
The job of formatting help printouts is delegated to a formatter object. You are
free to replace the formatter with a custom one by calling `formatter(fmt)` on
an `App`. CLI11 comes with a default App formatter, `Formatter`. You can
retrieve the formatter via `.get_formatter()` this will return a pointer to the
current `Formatter`. It is customizable; you can set `label(key, value)` to
replace the default labels like `REQUIRED`, and `OPTIONS`. You can also set
`column_width(n)` to set the width of the columns before you add the functional
to the app or option. Several other configuration options are also available in
the `Formatter`. You can also override almost any stage of the formatting
process in a subclass of either formatter. If you want to make a new formatter
from scratch, you can do that too; you just need to implement the correct
signature. see [formatting][] for more details.
### Subclassing
@@ -1997,3 +1995,5 @@ try! Feedback is always welcome.
[toml]: https://toml.io
[lyra]: https://github.com/bfgroup/Lyra
[installation]: https://cliutils.github.io/CLI11/book/chapters/installation.html
[formatting]: https://cliutils.github.io/CLI11/book/chapters/formatting.html
[config]: https://cliutils.github.io/CLI11/book/chapters/config.html

View File

@@ -1,7 +1,5 @@
# Formatting help output
{% hint style='info' %} New in CLI11 1.6 {% endhint %}
## Customizing an existing formatter
In CLI11, you can control the output of the help printout in full or in part.
@@ -12,9 +10,18 @@ will be inherited by subcommands that are created after you set the formatter.
There are several configuration options that you can set:
| Set method | Description | Availability |
| --------------------- | -------------------------------- | ------------ |
| `column_width(width)` | The width of the columns | Both |
| ------------------------------------------ | -------------------------------------------------------------------- | ------------ |
| `column_width(width)` | The width of the columns (30) | Both |
| `label(key, value)` | Set a label to a different value | Both |
| `long_option_alignment_ratio(float)` | Set the alignment ratio for long options within the left column(1/3) | Both |
| `right_column_width(std::size_t)` | Set the right column width(65) | Both |
| `description_paragraph_width(std::size_t)` | Set the description paragraph width at the top of help(88) | Both |
| `footer_paragraph_width(std::size_t)` | Set the footer paragraph width (88) | Both |
| `enable_description_formatting(bool)` | enable/disable description paragraph formatting (true) | Both |
| `enable_footer_formatting(bool)` | enable/disable footer paragraph formatting (true) | Both |
| `enable_option_defaults(bool)` | enable/disable printing of option defaults (true) | Both |
| `enable_option_type_names(bool)` | enable/disable printing of option types (true) | Both |
| `enable_default_flag_values(bool)` | enable/disable printing of default flag values (true) | Both |
Labels will map the built in names and type names from key to value if present.
For example, if you wanted to change the width of the columns to 40 and the
@@ -25,6 +32,56 @@ app.get_formatter()->column_width(40);
app.get_formatter()->label("REQUIRED", "(MUST HAVE)");
```
Used labels are `REQUIRED`, `POSITIONALS`, `Usage`, `OPTIONS`, `SUBCOMMAND`,
`SUBCOMMANDS`, `Env`, `Needs`,`Excludes`, and any type name such as `TEXT`,
`INT`,`FLOAT` and others. Replacing these labels with new ones will use the
specified words in place of the label.
### Customization Option Descriptions
Some of the control parameters are visualized in Figure 1. They manage the
column widths and ratios of the different sections of the help
![example help output](../images/help_output1.png)
### long option alignment ratio
The long option alignment ratio controls the relative proportion of short to
long option names. It must be a number between 0 and 1. values entered outside
this range are converted into the range by absolute value or inversion. It
defines where in the left column long optiosn are aligned. It is a ratio of the
column width property.
### formatting options
There are occasions where it is necessary to disable the formatting for headers
and footers the two options `enable_description_formatting(false)` and
`enable_footer_formatting(false)` turn off any formatting on the description and
footer. This allows things like word art or external management of alignment and
width. With formatting enabled the width is enforced and the paragraphs
reflowed.
### Option output control
Additional control options manage printing of specific aspects of an option
```text
OPTIONS:
-h, --help Print this help message and exit
--opt TEXT [DEFFFF]
-o, --opt2 INT this is a description for opt2
-f, -n, --opt3, --option-double FLOAT
this is a description for option3
--flag, --no_flag{false}
a flag option with a negative flag as well
```
The `[DEFFFF]` portion, which is the default value for options if specified can
be turned off in the help output through `enable_option_defaults(false)`. The
`TEXT`, `INT`, `FLOAT` or other type names can be turned off via
`enable_option_type_names(false)`. and the `{false}` or flag default values can
be turned off using `enable_default_flag_values(false)`.
## Subclassing
You can further configure pieces of the code while still keeping most of the
@@ -84,3 +141,24 @@ Notes:
- `*1`: This signature depends on whether the call is from a positional or
optional.
- `o` is opt pointer, `p` is true if positional.
## formatting callback
For certain cases it is useful to use a callback for the help formatting
```c++
app.formatter_fn(
[](const CLI::App *, std::string, CLI::AppFormatMode) { return std::string("This is really simple"); });
```
This callback replaces the make_help call in the formatter with the callback.
This is a wrapper around a custom formatter that just needs the main call. All
configuration options are available but are ignored as the output is purely
driven by the callback. The first argument is a const pointer to the App in
question. The formatter will get a std::string usage name as the second option,
and a AppFormatMode mode for the final option. It should return a std::string.
The `AppFormatMode` can be `Normal`, `All`, or `Sub`, and it indicates the
situation the help was called in. `Sub` is optional, but the default formatter
uses it to make sure expanded subcommands are called with their own formatter
since you can't access anything but the call operator once a formatter has been
set.

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 KiB

View File

@@ -63,6 +63,10 @@ class FormatterBase {
bool enable_description_formatting_{true};
bool enable_footer_formatting_{true};
/// options controlling formatting of options
bool enable_option_defaults_{true};
bool enable_option_type_names_{true};
bool enable_default_flag_values_{true};
/// @brief The required help printout labels (user changeable)
/// Values are Needs, Excludes, etc.
std::map<std::string, std::string> labels_{};
@@ -96,7 +100,10 @@ class FormatterBase {
/// Set the alignment ratio for long options within the left column
/// The ratio is in [0;1] range (e.g. 0.2 = 20% of column width, 6.f/column_width = 6th character)
void long_option_alignment_ratio(float ratio) { long_option_alignment_ratio_ = ratio; }
void long_option_alignment_ratio(float ratio) {
long_option_alignment_ratio_ =
(ratio >= 0.0f) ? ((ratio <= 1.0f) ? ratio : 1.0f / ratio) : ((ratio < -1.0f) ? 1.0f / (-ratio) : -ratio);
}
/// Set the right column width (description of options/flags/subcommands)
void right_column_width(std::size_t val) { right_column_width_ = val; }
@@ -110,6 +117,13 @@ class FormatterBase {
void enable_description_formatting(bool value = true) { enable_description_formatting_ = value; }
/// disable formatting for footer paragraph
void enable_footer_formatting(bool value = true) { enable_footer_formatting_ = value; }
/// enable option defaults to be printed
void enable_option_defaults(bool value = true) { enable_option_defaults_ = value; }
/// enable option type names to be printed
void enable_option_type_names(bool value = true) { enable_option_type_names_ = value; }
/// enable default flag values to be printed
void enable_default_flag_values(bool value = true) { enable_default_flag_values_ = value; }
///@}
/// @name Getters
///@{
@@ -133,12 +147,25 @@ class FormatterBase {
/// Get the current footer paragraph width
CLI11_NODISCARD std::size_t get_footer_paragraph_width() const { return footer_paragraph_width_; }
/// @brief Get the current alignment ratio for long options within the left column
/// @return
CLI11_NODISCARD float get_long_option_alignment_ratio() const { return long_option_alignment_ratio_; }
/// Get the current status of description paragraph formatting
CLI11_NODISCARD bool is_description_paragraph_formatting_enabled() const { return enable_description_formatting_; }
/// Get the current status of whether footer paragraph formatting is enabled
CLI11_NODISCARD bool is_footer_paragraph_formatting_enabled() const { return enable_footer_formatting_; }
/// Get the current status of whether option defaults are printed
CLI11_NODISCARD bool is_option_defaults_enabled() const { return enable_option_defaults_; }
/// Get the current status of whether option type names are printed
CLI11_NODISCARD bool is_option_type_names_enabled() const { return enable_option_type_names_; }
/// Get the current status of whether default flag values are printed
CLI11_NODISCARD bool is_default_flag_values_enabled() const { return enable_default_flag_values_; }
///@}
};

View File

@@ -657,8 +657,10 @@ class Option : public OptionBase<Option> {
/// Will include / prefer the positional name if positional is true.
/// If all_options is false, pick just the most descriptive name to show.
/// Use `get_name(true)` to get the positional name (replaces `get_pname`)
/// if disable_default_flag_values is true, do not include the default values for flags such as `--no-flag{false}`
CLI11_NODISCARD std::string get_name(bool positional = false, ///< Show the positional name
bool all_options = false ///< Show every option
bool all_options = false, ///< Show every option
bool disable_default_flag_values = false ///< Disable default values in name
) const;
///@}

View File

@@ -368,7 +368,7 @@ CLI11_INLINE std::string Formatter::make_option_name(const Option *opt, bool is_
if(is_positional)
return opt->get_name(true, false);
return opt->get_name(false, true);
return opt->get_name(false, true, !enable_default_flag_values_);
}
CLI11_INLINE std::string Formatter::make_option_opts(const Option *opt) const {
@@ -378,10 +378,14 @@ CLI11_INLINE std::string Formatter::make_option_opts(const Option *opt) const {
out << " " << opt->get_option_text();
} else {
if(opt->get_type_size() != 0) {
if(enable_option_type_names_) {
if(!opt->get_type_name().empty())
out << " " << get_label(opt->get_type_name());
}
if(enable_option_defaults_) {
if(!opt->get_default_str().empty())
out << " [" << opt->get_default_str() << "] ";
}
if(opt->get_expected_max() == detail::expected_max_vector_size)
out << " ...";
else if(opt->get_expected_min() > 1)

View File

@@ -254,7 +254,8 @@ CLI11_INLINE Option *Option::multi_option_policy(MultiOptionPolicy value) {
return this;
}
CLI11_NODISCARD CLI11_INLINE std::string Option::get_name(bool positional, bool all_options) const {
CLI11_NODISCARD CLI11_INLINE std::string
Option::get_name(bool positional, bool all_options, bool disable_default_flag_values) const {
if(get_group().empty())
return {}; // Hidden
@@ -269,14 +270,14 @@ CLI11_NODISCARD CLI11_INLINE std::string Option::get_name(bool positional, bool
if((get_items_expected() == 0) && (!fnames_.empty())) {
for(const std::string &sname : snames_) {
name_list.push_back("-" + sname);
if(check_fname(sname)) {
if(!disable_default_flag_values && check_fname(sname)) {
name_list.back() += "{" + get_flag_value(sname, "") + "}";
}
}
for(const std::string &lname : lnames_) {
name_list.push_back("--" + lname);
if(check_fname(lname)) {
if(!disable_default_flag_values && check_fname(lname)) {
name_list.back() += "{" + get_flag_value(lname, "") + "}";
}
}

View File

@@ -95,6 +95,63 @@ TEST_CASE("Formatter: OptCustomizeOptionText", "[formatter]") {
CHECK_THAT(help, Contains("(ARG)"));
}
TEST_CASE("Formatter: OptBaseExample", "[formatter]") {
CLI::App app{"My prog"};
app.get_formatter()->column_width(25);
std::string v{};
app.add_option("--opt", v)->default_str("DEFFFF");
int v2{0};
app.add_option("-o,--opt2", v2, "this is a description for opt2");
double v3{0.0};
app.add_option("-f,-n,--opt3,--option-double", v3, "this is a description for option3");
app.add_flag("--flag,!--no_flag", "a flag option with a negative flag as well");
std::string help = app.help();
CHECK_THAT(help, Contains("DEFFFF"));
CHECK_THAT(help, Contains("{false"));
}
TEST_CASE("Formatter: OptDefaults", "[formatter]") {
CLI::App app{"My prog"};
app.get_formatter()->column_width(25);
std::string v{};
app.add_option("--opt", v)->default_str("DEFFFF");
std::string help = app.help();
CHECK_THAT(help, Contains("[DEFFFF]"));
app.get_formatter()->enable_option_defaults(false);
help = app.help();
CHECK_THAT(help, !Contains("[DEFFFF]"));
CHECK(!app.get_formatter()->is_option_defaults_enabled());
}
TEST_CASE("Formatter: OptTypes", "[formatter]") {
CLI::App app{"My prog"};
app.get_formatter()->column_width(25);
std::string v{};
app.add_option("--opt", v);
std::string help = app.help();
CHECK_THAT(help, Contains("TEXT"));
app.get_formatter()->enable_option_type_names(false);
help = app.help();
CHECK_THAT(help, !Contains("TEXT"));
CHECK(!app.get_formatter()->is_option_type_names_enabled());
}
TEST_CASE("Formatter: FalseFlagExample", "[formatter]") {
CLI::App app{"My prog"};
@@ -114,6 +171,30 @@ TEST_CASE("Formatter: FalseFlagExample", "[formatter]") {
CHECK_THAT(help, Contains("-O{false}"));
}
TEST_CASE("Formatter: FalseFlagExampleDisable", "[formatter]") {
CLI::App app{"My prog"};
app.get_formatter()->column_width(25);
int v{0};
app.add_flag("--opt,!--no_opt", v, "Something");
bool flag{false};
app.add_flag("!-O,--opt2,--no_opt2{false}", flag, "Something else");
std::string help = app.help();
CHECK_THAT(help, Contains("--no_opt{false}"));
CHECK_THAT(help, Contains("--no_opt2{false}"));
CHECK_THAT(help, Contains("-O{false}"));
app.get_formatter()->enable_default_flag_values(false);
CHECK(!app.get_formatter()->is_default_flag_values_enabled());
help = app.help();
CHECK_THAT(help, !Contains("{false}"));
}
TEST_CASE("Formatter: AppCustomize", "[formatter]") {
CLI::App app{"My prog"};
app.add_subcommand("subcom1", "This");