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 7 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
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Expand Up @@ -28,7 +28,7 @@ jobs:
name: Test
strategy:
matrix:
build: [linux, windows, mac, minimal, default]
build: [linux, windows, mac, minimal, default, next]
include:
- build: linux
os: ubuntu-latest
Expand All @@ -50,6 +50,10 @@ jobs:
os: ubuntu-latest
rust: "stable"
features: "default"
- build: next
os: ubuntu-latest
rust: "stable"
features: "next"
continue-on-error: ${{ matrix.rust != 'stable' }}
runs-on: ${{ matrix.os }}
steps:
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/rust-next.yml
Expand Up @@ -7,7 +7,7 @@ jobs:
name: Test
strategy:
matrix:
build: [stable, linux, windows, mac, nightly, minimal, default]
build: [stable, linux, windows, mac, nightly, minimal, default, next]
include:
- build: stable
os: ubuntu-latest
Expand Down Expand Up @@ -37,6 +37,10 @@ jobs:
os: ubuntu-latest
rust: "stable"
features: "default"
- build: next
os: ubuntu-latest
rust: "stable"
features: "next"
continue-on-error: ${{ matrix.rust != 'stable' }}
runs-on: ${{ matrix.os }}
steps:
Expand Down
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: 1 addition & 4 deletions clap_complete/src/shells/zsh.rs
Expand Up @@ -360,10 +360,7 @@ fn get_args_of(parent: &Command, p_global: Option<&Command>) -> String {
// Uses either `possible_vals` or `value_hint` to give hints about possible argument values
fn value_completion(arg: &Arg) -> Option<String> {
if let Some(values) = &arg.get_possible_values() {
if values
.iter()
.any(|value| !value.is_hide_set() && value.get_help().is_some())
{
if values.iter().any(|value| value.should_show_help()) {
Some(format!(
"(({}))",
values
Expand Down
30 changes: 29 additions & 1 deletion src/build/possible_value.rs
@@ -1,4 +1,4 @@
use std::iter;
use std::{borrow::Cow, iter};

use crate::util::eq_ignore_case;

Expand Down Expand Up @@ -148,6 +148,17 @@ impl<'help> PossibleValue<'help> {
self.help
}

/// Get the help specified for this argument, if any and the argument
/// value is not hidden
#[inline]
pub fn get_visible_help(&self) -> Option<&'help str> {
ModProg marked this conversation as resolved.
Show resolved Hide resolved
if !self.hide {
self.help
} else {
None
}
}

/// Deprecated, replaced with [`PossibleValue::is_hide_set`]
#[inline]
#[deprecated(since = "3.1.0", note = "Replaced with `PossibleValue::is_hide_set`")]
Expand All @@ -161,6 +172,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 All @@ -170,6 +186,18 @@ impl<'help> PossibleValue<'help> {
}
}

/// Get the name if argument value is not hidden, `None` otherwise,
/// but wrapped in quotes if it contains whitespace
pub fn get_visible_quoted_name(&self) -> Option<Cow<'help, str>> {
self.get_visible_name().map(|name| {
if name.contains(char::is_whitespace) {
format!("{:?}", name).into()
} else {
name.into()
}
})
}

/// Returns all valid values of the argument value.
///
/// Namely the name and all aliases.
Expand Down
128 changes: 101 additions & 27 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,98 @@ 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.is_hide_possible_values_set()
&& 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_quoted_name().map(|name| display_width(&name)))
.max()
.expect("Only called with possible value");
let help_longest = arg
.possible_vals
.iter()
.filter_map(|f| f.get_visible_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 && self.term_w < taken + 2 + help_longest; // 2 = ": "

let spaces = spaces + TAB_WIDTH - 2; // 2 = "- "
let spaces_help = if possible_value_new_line {
spaces + 2 // 2 = "- "
} else {
spaces + longest + 4 // 4 = "- " + ": "
};
ModProg marked this conversation as resolved.
Show resolved Hide resolved

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(": ")?;
// To align help messages
self.spaces(longest - display_width(pv.get_name()))?;
}

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 +538,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 +648,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 All @@ -581,15 +662,7 @@ impl<'help, 'cmd, 'writer> Help<'help, 'cmd, 'writer> {
let pvs = a
.possible_vals
.iter()
.filter_map(|value| {
if value.is_hide_set() {
None
} else if value.get_name().contains(char::is_whitespace) {
Some(format!("{:?}", value.get_name()))
} else {
Some(value.get_name().to_string())
}
})
.filter_map(PossibleValue::get_visible_quoted_name)
.collect::<Vec<_>>()
.join(", ");

Expand Down Expand Up @@ -670,7 +743,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 +1084,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