Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Possible Value help #3503

Merged
merged 8 commits into from Mar 2, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -81,6 +81,7 @@ unicode = ["textwrap/unicode-width", "unicase"] # Support for unicode character
unstable-replace = []
unstable-multicall = []
unstable-grouped = []
unstable-v4 = []

[lib]
bench = false
Expand Down
1 change: 1 addition & 0 deletions Makefile
Expand Up @@ -15,6 +15,7 @@ _FEATURES_minimal = --no-default-features --features "std"
_FEATURES_default =
_FEATURES_wasm = --features "derive cargo env unicode yaml regex unstable-replace unstable-multicall unstable-grouped"
_FEATURES_full = --features "derive cargo env unicode yaml regex unstable-replace unstable-multicall unstable-grouped wrap_help"
_FEATURES_next = ${_FEATURES_full} --features unstable-v4
ModProg marked this conversation as resolved.
Show resolved Hide resolved
_FEATURES_debug = ${_FEATURES_full} --features debug
_FEATURES_release = ${_FEATURES_full} --release

Expand Down
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -153,6 +153,7 @@ Why use the procedural [Builder API](https://github.com/clap-rs/clap/blob/v3.1.1
* **unstable-replace**: Enable [`Command::replace`](https://github.com/clap-rs/clap/issues/2836)
* **unstable-multicall**: Enable [`Command::multicall`](https://github.com/clap-rs/clap/issues/2861)
* **unstable-grouped**: Enable [`ArgMatches::grouped_values_of`](https://github.com/clap-rs/clap/issues/2924)
* unstable-v4: Show help messages for possible values in long help [#3312](https://github.com/clap-rs/clap/issues/3312)
ModProg marked this conversation as resolved.
Show resolved Hide resolved

## Sponsors

Expand Down
5 changes: 5 additions & 0 deletions src/build/possible_value.rs
Expand Up @@ -161,6 +161,11 @@ impl<'help> PossibleValue<'help> {
self.hide
}

/// Report if PossibleValue is not hidden and has a help message
pub fn should_show_help(&self) -> bool {
!self.hide && self.help.is_some()
}

/// Get the name if argument value is not hidden, `None` otherwise
pub fn get_visible_name(&self) -> Option<&'help str> {
if self.hide {
Expand Down
116 changes: 98 additions & 18 deletions src/output/help.rs
Expand Up @@ -11,6 +11,7 @@ use std::{
use crate::{
build::{display_arg_val, Arg, Command},
output::{fmt::Colorizer, Usage},
PossibleValue,
};

// Third party
Expand Down Expand Up @@ -385,7 +386,7 @@ impl<'help, 'cmd, 'writer> Help<'help, 'cmd, 'writer> {
/// Writes argument's help to the wrapped stream.
fn help(
&mut self,
is_not_positional: bool,
arg: Option<&Arg<'help>>,
about: &str,
spec_vals: &str,
next_line_help: bool,
Expand All @@ -401,7 +402,7 @@ impl<'help, 'cmd, 'writer> Help<'help, 'cmd, 'writer> {
longest + 12
};

let too_long = spaces + display_width(about) + display_width(spec_vals) >= self.term_w;
let too_long = spaces + display_width(&help) >= self.term_w;

// Is help on next line, if so then indent
if next_line_help {
Expand All @@ -423,17 +424,96 @@ impl<'help, 'cmd, 'writer> Help<'help, 'cmd, 'writer> {
if let Some(part) = help.lines().next() {
self.none(part)?;
}

// indent of help
let spaces = if next_line_help {
TAB_WIDTH * 3
} else if let Some(true) = arg.map(|a| a.is_positional()) {
longest + TAB_WIDTH * 2
} else {
longest + TAB_WIDTH * 3
};

for part in help.lines().skip(1) {
self.none("\n")?;
if next_line_help {
self.none(format!("{}{}{}", TAB, TAB, TAB))?;
} else if is_not_positional {
self.spaces(longest + 12)?;
} else {
self.spaces(longest + 8)?;
}
self.spaces(spaces)?;
self.none(part)?;
}

#[cfg(feature = "unstable-v4")]
if let Some(arg) = arg {
if self.use_long
&& arg
.possible_vals
.iter()
.any(PossibleValue::should_show_help)
{
ModProg marked this conversation as resolved.
Show resolved Hide resolved
debug!("Help::help: Found possible vals...{:?}", arg.possible_vals);
if !help.is_empty() {
self.none("\n\n")?;
self.spaces(spaces)?;
}
self.none("Possible values:")?;
let longest = arg
.possible_vals
.iter()
.filter_map(|f| f.get_visible_name().map(display_width))
.max()
.expect("Only called with possible value");
let help_longest = arg
.possible_vals
.iter()
.filter_map(|f| f.get_help().map(display_width))
.max()
.expect("Only called with possible value with help");
// should new line
let taken = longest + spaces + 2; // 2 = "- "
let possible_value_new_line = self.term_w >= taken
&& (taken as f32 / self.term_w as f32) > 0.60
&& help_longest + 2 // 2 = ": "
> (self.term_w - taken);

let spaces = spaces + TAB_WIDTH;
let spaces_help = if possible_value_new_line {
spaces + TAB_WIDTH
} else {
spaces + longest + 4 // 2 = "- " + ": "
};

for pv in arg.possible_vals.iter().filter(|pv| !pv.is_hide_set()) {
self.none("\n")?;
self.spaces(spaces)?;
self.none("- ")?;
self.good(pv.get_name())?;
if let Some(help) = pv.get_help() {
debug!("Help::help: Possible Value help");

if possible_value_new_line {
self.none(":\n")?;
self.spaces(spaces_help)?;
} else {
self.none(": ")?;
}

let avail_chars = if self.term_w > spaces_help {
self.term_w - spaces_help
} else {
usize::MAX
};

let help = text_wrapper(help, avail_chars);
let mut help = help.lines();

self.none(help.next().unwrap_or_default())?;
for part in help {
self.none("\n")?;
self.spaces(spaces_help)?;
self.none(part)?;
}
}
}
}
}
Ok(())
}

Expand All @@ -456,13 +536,7 @@ impl<'help, 'cmd, 'writer> Help<'help, 'cmd, 'writer> {
arg.help.or(arg.long_help).unwrap_or("")
};

self.help(
!arg.is_positional(),
about,
spec_vals,
next_line_help,
longest,
)?;
self.help(Some(arg), about, spec_vals, next_line_help, longest)?;
Ok(())
}

Expand Down Expand Up @@ -572,7 +646,12 @@ impl<'help, 'cmd, 'writer> Help<'help, 'cmd, 'writer> {
}
}

if !a.is_hide_possible_values_set() && !a.possible_vals.is_empty() {
if !(a.is_hide_possible_values_set()
|| a.possible_vals.is_empty()
|| cfg!(feature = "unstable-v4")
&& self.use_long
&& a.possible_vals.iter().any(PossibleValue::should_show_help))
{
debug!(
"Help::spec_vals: Found possible vals...{:?}",
a.possible_vals
Expand Down Expand Up @@ -670,7 +749,7 @@ impl<'help, 'cmd, 'writer> Help<'help, 'cmd, 'writer> {
.unwrap_or("");

self.subcmd(sc_str, next_line_help, longest)?;
self.help(false, about, spec_vals, next_line_help, longest)
self.help(None, about, spec_vals, next_line_help, longest)
epage marked this conversation as resolved.
Show resolved Hide resolved
}

fn sc_spec_vals(&self, a: &Command) -> String {
Expand Down Expand Up @@ -1011,6 +1090,7 @@ pub(crate) fn dimensions() -> Option<(usize, usize)> {
}

const TAB: &str = " ";
const TAB_WIDTH: usize = 4;

pub(crate) enum HelpWriter<'writer> {
Normal(&'writer mut dyn Write),
Expand Down
8 changes: 6 additions & 2 deletions src/parse/parser.rs
Expand Up @@ -8,7 +8,6 @@ use std::{
use os_str_bytes::RawOsStr;

// Internal
use crate::build::AppSettings as AS;
use crate::build::{Arg, Command};
use crate::error::Error as ClapError;
use crate::error::Result as ClapResult;
Expand All @@ -18,6 +17,7 @@ use crate::parse::features::suggestions;
use crate::parse::{ArgMatcher, SubCommand};
use crate::parse::{Validator, ValueSource};
use crate::util::{color::ColorChoice, Id};
use crate::{build::AppSettings as AS, PossibleValue};
use crate::{INTERNAL_ERROR_MSG, INVALID_UTF8};

pub(crate) struct Parser<'help, 'cmd> {
Expand Down Expand Up @@ -786,7 +786,11 @@ impl<'help, 'cmd> Parser<'help, 'cmd> {
// specified by the user is sent through. If hide_short_help is not included,
// then items specified with hidden_short_help will also be hidden.
let should_long = |v: &Arg| {
v.long_help.is_some() || v.is_hide_long_help_set() || v.is_hide_short_help_set()
v.long_help.is_some()
|| v.is_hide_long_help_set()
|| v.is_hide_short_help_set()
|| cfg!(feature = "unstable-v4")
&& v.possible_vals.iter().any(PossibleValue::should_show_help)
};

// Subcommands aren't checked because we prefer short help for them, deferring to
Expand Down
131 changes: 131 additions & 0 deletions tests/builder/help.rs
Expand Up @@ -246,6 +246,41 @@ OPTIONS:

static HIDE_POS_VALS: &str = "ctest 0.1

USAGE:
ctest [OPTIONS]

OPTIONS:
-c, --cafe <FILE> A coffeehouse, coffee shop, or café.
-h, --help Print help information
-p, --pos <VAL> Some vals [possible values: fast, slow]
-V, --version Print version information
";
#[cfg(feature = "unstable-v4")]
static POS_VALS_HELP: &str = "ctest 0.1

USAGE:
ctest [OPTIONS]

OPTIONS:
-c, --cafe <FILE>
A coffeehouse, coffee shop, or café.

-h, --help
Print help information

-p, --pos <VAL>
Some vals

Possible values:
- fast
- slow: not as fast

-V, --version
Print version information
";
#[cfg(not(feature = "unstable-v4"))]
static POS_VALS_HELP: &str = "ctest 0.1
ModProg marked this conversation as resolved.
Show resolved Hide resolved

USAGE:
ctest [OPTIONS]

Expand Down Expand Up @@ -964,6 +999,71 @@ OPTIONS:
));
}

#[test]
#[cfg(all(feature = "wrap_help"))]
fn possible_value_wrapped_help() {
#[cfg(feature = "unstable-v4")]
static WRAPPED_HELP: &str = "test

USAGE:
test [OPTIONS]

OPTIONS:
-h, --help
Print help information

--possible-values <possible_values>
Possible values:
- name: Long enough help message to clearly
warrant wrapping
- second
ModProg marked this conversation as resolved.
Show resolved Hide resolved

--possible-values-with-new-line <possible_values_with_new_line>
Possible values:
- long enough name to trigger new line:
Long enough help message to clearly warrant
ModProg marked this conversation as resolved.
Show resolved Hide resolved
wrapping
- second
ModProg marked this conversation as resolved.
Show resolved Hide resolved
";
#[cfg(not(feature = "unstable-v4"))]
static WRAPPED_HELP: &str = r#"test

USAGE:
test [OPTIONS]

OPTIONS:
-h, --help Print help information
--possible-values <possible_values> [possible values: name, second]
--possible-values-with-new-line <possible_values_with_new_line> [possible values: "long enough name to trigger new line", second]
ModProg marked this conversation as resolved.
Show resolved Hide resolved
"#;
let cmd = Command::new("test")
.term_width(67)
.arg(
Arg::new("possible_values")
.long("possible-values")
.possible_value(
PossibleValue::new("name")
.help("Long enough help message to clearly warrant wrapping"),
)
.possible_value("second"),
)
.arg(
Arg::new("possible_values_with_new_line")
.long("possible-values-with-new-line")
.possible_value(
PossibleValue::new("long enough name to trigger new line")
.help("Long enough help message to clearly warrant wrapping"),
)
.possible_value("second"),
);
assert!(utils::compare_output(
cmd,
"test --help",
WRAPPED_HELP,
false
));
}

#[test]
fn complex_subcommand_help_output() {
let a = utils::complex_app();
Expand Down Expand Up @@ -1061,6 +1161,37 @@ fn hide_single_possible_val() {
));
}

#[test]
fn possible_vals_with_help() {
let app = Command::new("ctest")
.version("0.1")
.arg(
Arg::new("pos")
.short('p')
.long("pos")
.value_name("VAL")
.possible_value("fast")
.possible_value(PossibleValue::new("slow").help("not as fast"))
.possible_value(PossibleValue::new("secret speed").hide(true))
.help("Some vals")
.takes_value(true),
)
.arg(
Arg::new("cafe")
.short('c')
.long("cafe")
.value_name("FILE")
.help("A coffeehouse, coffee shop, or café.")
.takes_value(true),
);
assert!(utils::compare_output(
app,
"ctest --help",
POS_VALS_HELP,
false
));
}

#[test]
fn issue_626_panic() {
let cmd = Command::new("ctest")
Expand Down