diff --git a/src/build/possible_value.rs b/src/build/possible_value.rs index bf611aa9523..6c205dc4d8a 100644 --- a/src/build/possible_value.rs +++ b/src/build/possible_value.rs @@ -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 { diff --git a/src/output/help.rs b/src/output/help.rs index 7f5fb3bd7bc..0a9ace45c20 100644 --- a/src/output/help.rs +++ b/src/output/help.rs @@ -11,6 +11,7 @@ use std::{ use crate::{ build::{display_arg_val, Arg, Command}, output::{fmt::Colorizer, Usage}, + PossibleValue, }; // Third party @@ -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, @@ -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 { @@ -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) + { + 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(()) } @@ -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(()) } @@ -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 @@ -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) } fn sc_spec_vals(&self, a: &Command) -> String { @@ -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), diff --git a/src/parse/parser.rs b/src/parse/parser.rs index 9321b2822c5..0b515b51a53 100644 --- a/src/parse/parser.rs +++ b/src/parse/parser.rs @@ -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; @@ -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> { @@ -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 diff --git a/tests/builder/help.rs b/tests/builder/help.rs index d07829a26eb..17789d371e1 100644 --- a/tests/builder/help.rs +++ b/tests/builder/help.rs @@ -246,6 +246,41 @@ OPTIONS: static HIDE_POS_VALS: &str = "ctest 0.1 +USAGE: + ctest [OPTIONS] + +OPTIONS: + -c, --cafe A coffeehouse, coffee shop, or café. + -h, --help Print help information + -p, --pos 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 + A coffeehouse, coffee shop, or café. + + -h, --help + Print help information + + -p, --pos + 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 + USAGE: ctest [OPTIONS] @@ -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: + - name: Long enough help message to clearly + warrant wrapping + - second + + --possible-values-with-new-line + Possible values: + - long enough name to trigger new line: + Long enough help message to clearly warrant + wrapping + - second +"; + #[cfg(not(feature = "unstable-v4"))] + static WRAPPED_HELP: &str = r#"test + +USAGE: + test [OPTIONS] + +OPTIONS: + -h, --help Print help information + --possible-values [possible values: name, second] + --possible-values-with-new-line [possible values: "long enough name to trigger new line", second] +"#; + 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(); @@ -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")