Skip to content

Commit

Permalink
feat(help): show help for possible values
Browse files Browse the repository at this point in the history
  • Loading branch information
ModProg committed Feb 23, 2022
1 parent fb8f235 commit 336cfb1
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 20 deletions.
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)
{
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)
}

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
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
--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
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> [possible values: name, second]
--possible-values-with-new-line <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();
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

0 comments on commit 336cfb1

Please sign in to comment.