Skip to content

Commit 05cd057

Browse files
committed
perf: Avoid retrieving possible_values unless used
In some sophisticated situations, these may be expensive to calculate. One example might be a '--branch' option accepting any single Git branch that exists on the remote -- in such a case, the remote would need to be queried for all possible_values. The cost is ultimately unavoidable at runtime since this validation has to happen eventually, but there's no need to pay it when generating help text if `is_hide_possible_values_set`. To keep '-h' fast, avoid collecting `possible_values` during '-h' unless we're actually going to use the values in display. This optimization is repeated for the manpage renderer. This is trivially based on the short-circuiting logic at [1], which at least supports the idea that actually consuming the iterator is not generally-guaranteed behavior when `hide_possible_values` is set. Note on the 'expensive' mod: This keeps all the possible_values tests in one file but allows the entire set of tests to be controlled by the 'strings' feature (which is required to be able to use String rather than str for each possible value). [1]: clap_builder/src/builder/command.rs:long_help_exists_
1 parent d092896 commit 05cd057

File tree

3 files changed

+205
-58
lines changed

3 files changed

+205
-58
lines changed

clap_builder/src/output/help_template.rs

Lines changed: 58 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -684,57 +684,56 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> {
684684
let help_is_empty = help.is_empty();
685685
self.writer.push_styled(&help);
686686
if let Some(arg) = arg {
687-
const DASH_SPACE: usize = "- ".len();
688-
let possible_vals = arg.get_possible_values();
689-
if !possible_vals.is_empty()
690-
&& !arg.is_hide_possible_values_set()
691-
&& self.use_long_pv(arg)
692-
{
693-
debug!("HelpTemplate::help: Found possible vals...{possible_vals:?}");
694-
let longest = possible_vals
695-
.iter()
696-
.filter(|f| !f.is_hide_set())
697-
.map(|f| display_width(f.get_name()))
698-
.max()
699-
.expect("Only called with possible value");
700-
701-
let spaces = spaces + TAB_WIDTH - DASH_SPACE;
702-
let trailing_indent = spaces + DASH_SPACE;
703-
let trailing_indent = self.get_spaces(trailing_indent);
687+
if !arg.is_hide_possible_values_set() && self.use_long_pv(arg) {
688+
const DASH_SPACE: usize = "- ".len();
689+
let possible_vals = arg.get_possible_values();
690+
if !possible_vals.is_empty() {
691+
debug!("HelpTemplate::help: Found possible vals...{possible_vals:?}");
692+
let longest = possible_vals
693+
.iter()
694+
.filter(|f| !f.is_hide_set())
695+
.map(|f| display_width(f.get_name()))
696+
.max()
697+
.expect("Only called with possible value");
698+
699+
let spaces = spaces + TAB_WIDTH - DASH_SPACE;
700+
let trailing_indent = spaces + DASH_SPACE;
701+
let trailing_indent = self.get_spaces(trailing_indent);
702+
703+
if !help_is_empty {
704+
let _ = write!(self.writer, "\n\n{:spaces$}", "");
705+
}
706+
self.writer.push_str("Possible values:");
707+
for pv in possible_vals.iter().filter(|pv| !pv.is_hide_set()) {
708+
let name = pv.get_name();
704709

705-
if !help_is_empty {
706-
let _ = write!(self.writer, "\n\n{:spaces$}", "");
707-
}
708-
self.writer.push_str("Possible values:");
709-
for pv in possible_vals.iter().filter(|pv| !pv.is_hide_set()) {
710-
let name = pv.get_name();
710+
let mut descr = StyledStr::new();
711+
let _ = write!(
712+
&mut descr,
713+
"{}{name}{}",
714+
literal.render(),
715+
literal.render_reset()
716+
);
717+
if let Some(help) = pv.get_help() {
718+
debug!("HelpTemplate::help: Possible Value help");
719+
// To align help messages
720+
let padding = longest - display_width(name);
721+
let _ = write!(&mut descr, ": {:padding$}", "");
722+
descr.push_styled(help);
723+
}
711724

712-
let mut descr = StyledStr::new();
713-
let _ = write!(
714-
&mut descr,
715-
"{}{name}{}",
716-
literal.render(),
717-
literal.render_reset()
718-
);
719-
if let Some(help) = pv.get_help() {
720-
debug!("HelpTemplate::help: Possible Value help");
721-
// To align help messages
722-
let padding = longest - display_width(name);
723-
let _ = write!(&mut descr, ": {:padding$}", "");
724-
descr.push_styled(help);
725+
let avail_chars = if self.term_w > trailing_indent.len() {
726+
self.term_w - trailing_indent.len()
727+
} else {
728+
usize::MAX
729+
};
730+
descr.replace_newline_var();
731+
descr.wrap(avail_chars);
732+
descr.indent("", &trailing_indent);
733+
734+
let _ = write!(self.writer, "\n{:spaces$}- ", "",);
735+
self.writer.push_styled(&descr);
725736
}
726-
727-
let avail_chars = if self.term_w > trailing_indent.len() {
728-
self.term_w - trailing_indent.len()
729-
} else {
730-
usize::MAX
731-
};
732-
descr.replace_newline_var();
733-
descr.wrap(avail_chars);
734-
descr.indent("", &trailing_indent);
735-
736-
let _ = write!(self.writer, "\n{:spaces$}- ", "",);
737-
self.writer.push_styled(&descr);
738737
}
739738
}
740739
}
@@ -844,17 +843,19 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> {
844843
spec_vals.push(format!("[short aliases: {als}]"));
845844
}
846845

847-
let possible_vals = a.get_possible_values();
848-
if !possible_vals.is_empty() && !a.is_hide_possible_values_set() && !self.use_long_pv(a) {
849-
debug!("HelpTemplate::spec_vals: Found possible vals...{possible_vals:?}");
846+
if !a.is_hide_possible_values_set() && !self.use_long_pv(a) {
847+
let possible_vals = a.get_possible_values();
848+
if !possible_vals.is_empty() {
849+
debug!("HelpTemplate::spec_vals: Found possible vals...{possible_vals:?}");
850850

851-
let pvs = possible_vals
852-
.iter()
853-
.filter_map(PossibleValue::get_visible_quoted_name)
854-
.collect::<Vec<_>>()
855-
.join(", ");
851+
let pvs = possible_vals
852+
.iter()
853+
.filter_map(PossibleValue::get_visible_quoted_name)
854+
.collect::<Vec<_>>()
855+
.join(", ");
856856

857-
spec_vals.push(format!("[possible values: {pvs}]"));
857+
spec_vals.push(format!("[possible values: {pvs}]"));
858+
}
858859
}
859860
let connector = if self.use_long { "\n" } else { " " };
860861
spec_vals.join(connector)

clap_mangen/src/render.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,11 +305,15 @@ fn option_default_values(opt: &clap::Arg) -> Option<String> {
305305
}
306306

307307
fn get_possible_values(arg: &clap::Arg) -> Option<(Vec<String>, bool)> {
308+
if arg.is_hide_possible_values_set() {
309+
return None;
310+
}
311+
308312
let possibles = &arg.get_possible_values();
309313
let possibles: Vec<&clap::builder::PossibleValue> =
310314
possibles.iter().filter(|pos| !pos.is_hide_set()).collect();
311315

312-
if !(possibles.is_empty() || arg.is_hide_possible_values_set()) {
316+
if !possibles.is_empty() {
313317
return Some(format_possible_values(&possibles));
314318
}
315319
None

tests/builder/possible_values.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,3 +472,145 @@ fn ignore_case_multiple_fail() {
472472
assert!(m.is_err());
473473
assert_eq!(m.unwrap_err().kind(), ErrorKind::InvalidValue);
474474
}
475+
476+
#[cfg(feature = "string")]
477+
mod expensive {
478+
use std::sync::{Arc, Mutex};
479+
480+
use clap::{Arg, Command};
481+
use clap_builder::builder::{PossibleValue, PossibleValuesParser, TypedValueParser};
482+
483+
#[cfg(feature = "error-context")]
484+
use super::utils;
485+
486+
#[derive(Clone)]
487+
struct ExpensiveValues {
488+
iterated: Arc<Mutex<bool>>,
489+
}
490+
491+
impl ExpensiveValues {
492+
pub fn new() -> Self {
493+
ExpensiveValues {
494+
iterated: Arc::new(Mutex::new(false)),
495+
}
496+
}
497+
}
498+
499+
impl IntoIterator for ExpensiveValues {
500+
type Item = String;
501+
502+
type IntoIter = ExpensiveValuesIntoIterator;
503+
504+
fn into_iter(self) -> Self::IntoIter {
505+
ExpensiveValuesIntoIterator { me: self, index: 0 }
506+
}
507+
}
508+
509+
struct ExpensiveValuesIntoIterator {
510+
me: ExpensiveValues,
511+
index: usize,
512+
}
513+
514+
impl Iterator for ExpensiveValuesIntoIterator {
515+
type Item = String;
516+
fn next(&mut self) -> Option<String> {
517+
let mut guard = self
518+
.me
519+
.iterated
520+
.lock()
521+
.expect("not working across multiple threads");
522+
523+
*guard = true;
524+
self.index += 1;
525+
526+
if self.index < 3 {
527+
Some(format!("expensive-value-{}", self.index))
528+
} else {
529+
None
530+
}
531+
}
532+
}
533+
534+
impl TypedValueParser for ExpensiveValues {
535+
type Value = String;
536+
537+
fn parse_ref(
538+
&self,
539+
_cmd: &clap_builder::Command,
540+
_arg: Option<&clap_builder::Arg>,
541+
_value: &std::ffi::OsStr,
542+
) -> Result<Self::Value, clap_builder::Error> {
543+
todo!()
544+
}
545+
546+
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
547+
Some(Box::new(self.clone().into_iter().map(PossibleValue::from)))
548+
}
549+
}
550+
551+
#[test]
552+
fn no_iterate_when_hidden() {
553+
static PV_EXPECTED: &str = "\
554+
Usage: clap-test [some-cheap-option] [some-expensive-option]
555+
556+
Arguments:
557+
[some-cheap-option] cheap [possible values: some, cheap, values]
558+
[some-expensive-option] expensive
559+
560+
Options:
561+
-h, --help Print help
562+
";
563+
let expensive = ExpensiveValues::new();
564+
utils::assert_output(
565+
Command::new("test")
566+
.arg(
567+
Arg::new("some-cheap-option")
568+
.help("cheap")
569+
.value_parser(PossibleValuesParser::new(["some", "cheap", "values"])),
570+
)
571+
.arg(
572+
Arg::new("some-expensive-option")
573+
.help("expensive")
574+
.hide_possible_values(true)
575+
.value_parser(expensive.clone()),
576+
),
577+
"clap-test -h",
578+
PV_EXPECTED,
579+
false,
580+
);
581+
assert_eq!(*expensive.iterated.lock().unwrap(), false);
582+
}
583+
584+
#[test]
585+
fn iterate_when_displayed() {
586+
static PV_EXPECTED: &str = "\
587+
Usage: clap-test [some-cheap-option] [some-expensive-option]
588+
589+
Arguments:
590+
[some-cheap-option] cheap [possible values: some, cheap, values]
591+
[some-expensive-option] expensive [possible values: expensive-value-1, expensive-value-2]
592+
593+
Options:
594+
-h, --help Print help
595+
";
596+
let expensive = ExpensiveValues::new();
597+
utils::assert_output(
598+
Command::new("test")
599+
.arg(
600+
Arg::new("some-cheap-option")
601+
.help("cheap")
602+
.value_parser(PossibleValuesParser::new(["some", "cheap", "values"])),
603+
)
604+
.arg(
605+
Arg::new("some-expensive-option")
606+
.help("expensive")
607+
.hide_possible_values(false)
608+
.value_parser(expensive.clone()),
609+
),
610+
"clap-test -h",
611+
PV_EXPECTED,
612+
false,
613+
);
614+
assert_eq!(*expensive.iterated.lock().unwrap(), true);
615+
}
616+
}

0 commit comments

Comments
 (0)