From 64ee0f80096def97096466a66e98adb9d19689e8 Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Wed, 8 Apr 2020 02:34:53 +0300 Subject: [PATCH] Add hinting of arg value types for zsh/fish completion Adds new method/attribute `Arg::value_hint`, taking a `ValueHint` enum as argument. The hint can denote accepted values, for example: paths, usernames, hostnames, commands, etc. This initial implementation supports hints for the zsh and fish completion generators, support for other shells can be added later. --- clap_generate/examples/value_hints.rs | 114 ++++++++++++++ clap_generate/src/generators/shells/fish.rs | 34 ++++- clap_generate/src/generators/shells/zsh.rs | 83 ++++++---- clap_generate/tests/completions.rs | 16 +- clap_generate/tests/value_hints.rs | 161 ++++++++++++++++++++ src/build/app/mod.rs | 16 +- src/build/arg/mod.rs | 57 +++++++ src/build/arg/value_hint.rs | 92 +++++++++++ src/build/macros.rs | 11 ++ src/build/mod.rs | 2 +- src/lib.rs | 2 +- tests/fixtures/app.yaml | 4 + tests/yaml.rs | 15 +- 13 files changed, 562 insertions(+), 45 deletions(-) create mode 100644 clap_generate/examples/value_hints.rs create mode 100644 clap_generate/tests/value_hints.rs create mode 100644 src/build/arg/value_hint.rs diff --git a/clap_generate/examples/value_hints.rs b/clap_generate/examples/value_hints.rs new file mode 100644 index 00000000000..33f3ef3f461 --- /dev/null +++ b/clap_generate/examples/value_hints.rs @@ -0,0 +1,114 @@ +//! Example to test arguments with different ValueHint values. +//! +//! Usage with zsh: +//! ```sh +//! cargo run --example value_hints -- --generate=zsh > /usr/local/share/zsh/site-functions/_value_hints +//! compinit +//! ./target/debug/examples/value_hints -- +//! ``` +//! fish: +//! ```sh +//! cargo run --example value_hints -- --generate=fish > value_hints.fish +//! . ./value_hints.fish +//! ./target/debug/examples/value_hints -- +//! ``` +use clap::{App, AppSettings, Arg, ValueHint}; +use clap_generate::generators::{Elvish, Fish, PowerShell, Zsh}; +use clap_generate::{generate, generators::Bash}; +use std::io; + +const APPNAME: &str = "value_hints"; + +fn build_cli() -> App<'static> { + App::new(APPNAME) + .setting(AppSettings::DisableVersion) + .setting(AppSettings::TrailingVarArg) + .arg(Arg::new("generator").long("generate").possible_values(&[ + "bash", + "elvish", + "fish", + "powershell", + "zsh", + ])) + .arg( + Arg::new("unknown") + .long("unknown") + .value_hint(ValueHint::Unknown), + ) + .arg(Arg::new("other").long("other").value_hint(ValueHint::Other)) + .arg( + Arg::new("path") + .long("path") + .short('p') + .value_hint(ValueHint::AnyPath), + ) + .arg( + Arg::new("file") + .long("file") + .short('f') + .value_hint(ValueHint::FilePath), + ) + .arg( + Arg::new("dir") + .long("dir") + .short('d') + .value_hint(ValueHint::DirPath), + ) + .arg( + Arg::new("exe") + .long("exe") + .short('e') + .value_hint(ValueHint::ExecutablePath), + ) + .arg( + Arg::new("cmd_name") + .long("cmd-name") + .value_hint(ValueHint::CommandName), + ) + .arg( + Arg::new("cmd") + .long("cmd") + .short('c') + .value_hint(ValueHint::CommandString), + ) + .arg( + Arg::new("command_with_args") + .multiple_values(true) + .value_hint(ValueHint::CommandWithArguments), + ) + .arg( + Arg::new("user") + .short('u') + .long("user") + .value_hint(ValueHint::Username), + ) + .arg( + Arg::new("host") + .short('h') + .long("host") + .value_hint(ValueHint::Hostname), + ) + .arg(Arg::new("url").long("url").value_hint(ValueHint::Url)) + .arg( + Arg::new("email") + .long("email") + .value_hint(ValueHint::EmailAddress), + ) +} + +fn main() { + let matches = build_cli().get_matches(); + + if let Some(generator) = matches.value_of("generator") { + let mut app = build_cli(); + eprintln!("Generating completion file for {}...", generator); + match generator { + "bash" => generate::(&mut app, APPNAME, &mut io::stdout()), + "elvish" => generate::(&mut app, APPNAME, &mut io::stdout()), + "fish" => generate::(&mut app, APPNAME, &mut io::stdout()), + "powershell" => generate::(&mut app, APPNAME, &mut io::stdout()), + "zsh" => generate::(&mut app, APPNAME, &mut io::stdout()), + _ => panic!("Unknown generator"), + } + } +} diff --git a/clap_generate/src/generators/shells/fish.rs b/clap_generate/src/generators/shells/fish.rs index d02f9d668db..823224624c6 100644 --- a/clap_generate/src/generators/shells/fish.rs +++ b/clap_generate/src/generators/shells/fish.rs @@ -6,6 +6,8 @@ use crate::Generator; use clap::*; /// Generate fish completion file +/// +/// Note: The fish generator currently only supports named options (-o/--option), not positional arguments. pub struct Fish; impl Generator for Fish { @@ -75,9 +77,7 @@ fn gen_fish_inner(root_command: &str, app: &App, buffer: &mut String) { template.push_str(format!(" -d '{}'", escape_string(data)).as_str()); } - if let Some(ref data) = option.get_possible_values() { - template.push_str(format!(" -r -f -a \"{}\"", data.join(" ")).as_str()); - } + template.push_str(value_completion(option).as_str()); buffer.push_str(template.as_str()); buffer.push_str("\n"); @@ -127,3 +127,31 @@ fn gen_fish_inner(root_command: &str, app: &App, buffer: &mut String) { gen_fish_inner(root_command, subcommand, buffer); } } + +fn value_completion(option: &Arg) -> String { + if !option.is_set(ArgSettings::TakesValue) { + return "".to_string(); + } + + if let Some(ref data) = option.get_possible_values() { + format!(" -r -f -a \"{}\"", data.join(" ")) + } else { + // NB! If you change this, please also update the table in `ValueHint` documentation. + match option.get_value_hint() { + ValueHint::Unknown => " -r", + // fish has no built-in support to distinguish these + ValueHint::AnyPath | ValueHint::FilePath | ValueHint::ExecutablePath => " -r -F", + ValueHint::DirPath => " -r -f -a \"(__fish_complete_directories)\"", + // It seems fish has no built-in support for completing command + arguments as + // single string (CommandString). Complete just the command name. + ValueHint::CommandString | ValueHint::CommandName => { + " -r -f -a \"(__fish_complete_command)\"" + } + ValueHint::Username => " -r -f -a \"(__fish_complete_users)\"", + ValueHint::Hostname => " -r -f -a \"(__fish_print_hostnames)\"", + // Disable completion for others + _ => " -r -f", + } + .to_string() + } +} diff --git a/clap_generate/src/generators/shells/zsh.rs b/clap_generate/src/generators/shells/zsh.rs index c8e615c6ddb..e32303d6714 100644 --- a/clap_generate/src/generators/shells/zsh.rs +++ b/clap_generate/src/generators/shells/zsh.rs @@ -330,6 +330,42 @@ fn get_args_of(p: &App) -> String { ret.join("\n") } +// Uses either `possible_vals` or `value_hint` to give hints about possible argument values +fn value_completion(arg: &Arg) -> Option { + if let Some(values) = &arg.get_possible_values() { + Some(format!( + "({})", + values + .iter() + .map(|&v| escape_value(v)) + .collect::>() + .join(" ") + )) + } else { + // NB! If you change this, please also update the table in `ValueHint` documentation. + Some( + match arg.get_value_hint() { + ValueHint::Unknown => { + return None; + } + ValueHint::Other => "( )", + ValueHint::AnyPath => "_files", + ValueHint::FilePath => "_files", + ValueHint::DirPath => "_files -/", + ValueHint::ExecutablePath => "_absolute_command_paths", + ValueHint::CommandName => "_command_names -e", + ValueHint::CommandString => "_cmdstring", + ValueHint::CommandWithArguments => "_cmdambivalent", + ValueHint::Username => "_users", + ValueHint::Hostname => "_hosts", + ValueHint::Url => "_urls", + ValueHint::EmailAddress => "_email_addresses", + } + .to_string(), + ) + } +} + // Escape help string inside single quotes and brackets fn escape_help(string: &str) -> String { string @@ -370,26 +406,18 @@ fn write_opts_of(p: &App) -> String { "" }; - let pv = if let Some(ref pv_vec) = o.get_possible_values() { - format!( - ": :({})", - pv_vec - .iter() - .map(|v| escape_value(*v)) - .collect::>() - .join(" ") - ) - } else { - String::new() + let vc = match value_completion(o) { + Some(val) => format!(": :{}", val), + None => "".to_string(), }; if let Some(short) = o.get_short() { let s = format!( - "'{conflicts}{multiple}-{arg}+[{help}]{possible_values}' \\", + "'{conflicts}{multiple}-{arg}+[{help}]{value_completion}' \\", conflicts = conflicts, multiple = multiple, arg = short, - possible_values = pv, + value_completion = vc, help = help ); @@ -399,11 +427,11 @@ fn write_opts_of(p: &App) -> String { if let Some(short_aliases) = o.get_visible_short_aliases() { for alias in short_aliases { let s = format!( - "'{conflicts}{multiple}-{arg}+[{help}]{possible_values}' \\", + "'{conflicts}{multiple}-{arg}+[{help}]{value_completion}' \\", conflicts = conflicts, multiple = multiple, arg = alias, - possible_values = pv, + value_completion = vc, help = help ); @@ -415,11 +443,11 @@ fn write_opts_of(p: &App) -> String { if let Some(long) = o.get_long() { let l = format!( - "'{conflicts}{multiple}--{arg}=[{help}]{possible_values}' \\", + "'{conflicts}{multiple}--{arg}=[{help}]{value_completion}' \\", conflicts = conflicts, multiple = multiple, arg = long, - possible_values = pv, + value_completion = vc, help = help ); @@ -525,15 +553,17 @@ fn write_positionals_of(p: &App) -> String { for arg in p.get_positionals() { debug!("write_positionals_of:iter: arg={}", arg.get_name()); - let optional = if !arg.is_set(ArgSettings::Required) { + let cardinality = if arg.is_set(ArgSettings::MultipleValues) { + "*:" + } else if !arg.is_set(ArgSettings::Required) { ":" } else { "" }; let a = format!( - "'{optional}:{name}{help}:{action}' \\", - optional = optional, + "'{cardinality}:{name}{help}:{value_completion}' \\", + cardinality = cardinality, name = arg.get_name(), help = arg .get_about() @@ -541,18 +571,7 @@ fn write_positionals_of(p: &App) -> String { .replace("[", "\\[") .replace("]", "\\]") .replace(":", "\\:"), - action = arg - .get_possible_values() - .map_or("_files".to_owned(), |values| { - format!( - "({})", - values - .iter() - .map(|v| escape_value(*v)) - .collect::>() - .join(" ") - ) - }) + value_completion = value_completion(arg).unwrap_or_else(|| "".to_string()) ); debug!("write_positionals_of:iter: Wrote...{}", a); diff --git a/clap_generate/tests/completions.rs b/clap_generate/tests/completions.rs index 8135ddf65f6..1aa76b31d42 100644 --- a/clap_generate/tests/completions.rs +++ b/clap_generate/tests/completions.rs @@ -1,4 +1,4 @@ -use clap::{App, Arg}; +use clap::{App, Arg, ValueHint}; use clap_generate::{generate, generators::*}; use std::fmt; @@ -182,7 +182,7 @@ static FISH: &str = r#"complete -c myapp -n "__fish_use_subcommand" -s h -l help complete -c myapp -n "__fish_use_subcommand" -s V -l version -d 'Prints version information' complete -c myapp -n "__fish_use_subcommand" -f -a "test" -d 'tests things' complete -c myapp -n "__fish_use_subcommand" -f -a "help" -d 'Prints this message or the help of the given subcommand(s)' -complete -c myapp -n "__fish_seen_subcommand_from test" -l case -d 'the case to test' +complete -c myapp -n "__fish_seen_subcommand_from test" -l case -d 'the case to test' -r complete -c myapp -n "__fish_seen_subcommand_from test" -s h -l help -d 'Prints help information' complete -c myapp -n "__fish_seen_subcommand_from test" -s V -l version -d 'Prints version information' complete -c myapp -n "__fish_seen_subcommand_from help" -s h -l help -d 'Prints help information' @@ -526,10 +526,10 @@ complete -c my_app -n "__fish_use_subcommand" -f -a "test" -d 'tests things' complete -c my_app -n "__fish_use_subcommand" -f -a "some_cmd" -d 'tests other things' complete -c my_app -n "__fish_use_subcommand" -f -a "some-cmd-with-hypens" complete -c my_app -n "__fish_use_subcommand" -f -a "help" -d 'Prints this message or the help of the given subcommand(s)' -complete -c my_app -n "__fish_seen_subcommand_from test" -l case -d 'the case to test' +complete -c my_app -n "__fish_seen_subcommand_from test" -l case -d 'the case to test' -r complete -c my_app -n "__fish_seen_subcommand_from test" -s h -l help -d 'Prints help information' complete -c my_app -n "__fish_seen_subcommand_from test" -s V -l version -d 'Prints version information' -complete -c my_app -n "__fish_seen_subcommand_from some_cmd" -l config -d 'the other case to test' +complete -c my_app -n "__fish_seen_subcommand_from some_cmd" -l config -d 'the other case to test' -r complete -c my_app -n "__fish_seen_subcommand_from some_cmd" -s h -l help -d 'Prints help information' complete -c my_app -n "__fish_seen_subcommand_from some_cmd" -s V -l version -d 'Prints version information' complete -c my_app -n "__fish_seen_subcommand_from some-cmd-with-hypens" -s h -l help -d 'Prints help information' @@ -719,7 +719,11 @@ fn build_app() -> App<'static> { fn build_app_with_name(s: &'static str) -> App<'static> { App::new(s) .about("Tests completions") - .arg(Arg::new("file").about("some input file")) + .arg( + Arg::new("file") + .value_hint(ValueHint::FilePath) + .about("some input file"), + ) .subcommand( App::new("test").about("tests things").arg( Arg::new("case") @@ -773,7 +777,7 @@ fn build_app_special_help() -> App<'static> { ) } -fn common(app: &mut App, name: &str, fixture: &str) { +pub fn common(app: &mut App, name: &str, fixture: &str) { let mut buf = vec![]; generate::(app, name, &mut buf); let string = String::from_utf8(buf).unwrap(); diff --git a/clap_generate/tests/value_hints.rs b/clap_generate/tests/value_hints.rs new file mode 100644 index 00000000000..83d55f30bcd --- /dev/null +++ b/clap_generate/tests/value_hints.rs @@ -0,0 +1,161 @@ +use clap::{App, AppSettings, Arg, ValueHint}; +use clap_generate::generators::*; +use completions::common; + +mod completions; + +pub fn build_app_with_value_hints() -> App<'static> { + App::new("my_app") + .setting(AppSettings::DisableVersion) + .setting(AppSettings::TrailingVarArg) + .arg( + Arg::new("choice") + .long("choice") + .possible_values(&["bash", "fish", "zsh"]), + ) + .arg( + Arg::new("unknown") + .long("unknown") + .value_hint(ValueHint::Unknown), + ) + .arg(Arg::new("other").long("other").value_hint(ValueHint::Other)) + .arg( + Arg::new("path") + .long("path") + .short('p') + .value_hint(ValueHint::AnyPath), + ) + .arg( + Arg::new("file") + .long("file") + .short('f') + .value_hint(ValueHint::FilePath), + ) + .arg( + Arg::new("dir") + .long("dir") + .short('d') + .value_hint(ValueHint::DirPath), + ) + .arg( + Arg::new("exe") + .long("exe") + .short('e') + .value_hint(ValueHint::ExecutablePath), + ) + .arg( + Arg::new("cmd_name") + .long("cmd-name") + .value_hint(ValueHint::CommandName), + ) + .arg( + Arg::new("cmd") + .long("cmd") + .short('c') + .value_hint(ValueHint::CommandString), + ) + .arg( + Arg::new("command_with_args") + .multiple_values(true) + .value_hint(ValueHint::CommandWithArguments), + ) + .arg( + Arg::new("user") + .short('u') + .long("user") + .value_hint(ValueHint::Username), + ) + .arg( + Arg::new("host") + .short('h') + .long("host") + .value_hint(ValueHint::Hostname), + ) + .arg(Arg::new("url").long("url").value_hint(ValueHint::Url)) + .arg( + Arg::new("email") + .long("email") + .value_hint(ValueHint::EmailAddress), + ) +} + +static ZSH_VALUE_HINTS: &str = r#"#compdef my_app + +autoload -U is-at-least + +_my_app() { + typeset -A opt_args + typeset -a _arguments_options + local ret=1 + + if is-at-least 5.2; then + _arguments_options=(-s -S -C) + else + _arguments_options=(-s -C) + fi + + local context curcontext="$curcontext" state line + _arguments "${_arguments_options[@]}" \ +'--choice=[]: :(bash fish zsh)' \ +'--unknown=[]' \ +'--other=[]: :( )' \ +'-p+[]: :_files' \ +'--path=[]: :_files' \ +'-f+[]: :_files' \ +'--file=[]: :_files' \ +'-d+[]: :_files -/' \ +'--dir=[]: :_files -/' \ +'-e+[]: :_absolute_command_paths' \ +'--exe=[]: :_absolute_command_paths' \ +'--cmd-name=[]: :_command_names -e' \ +'-c+[]: :_cmdstring' \ +'--cmd=[]: :_cmdstring' \ +'-u+[]: :_users' \ +'--user=[]: :_users' \ +'-h+[]: :_hosts' \ +'--host=[]: :_hosts' \ +'--url=[]: :_urls' \ +'--email=[]: :_email_addresses' \ +'--help[Prints help information]' \ +'*::command_with_args:_cmdambivalent' \ +&& ret=0 + +} + +(( $+functions[_my_app_commands] )) || +_my_app_commands() { + local commands; commands=( + + ) + _describe -t commands 'my_app commands' commands "$@" +} + +_my_app "$@""#; + +static FISH_VALUE_HINTS: &str = r#"complete -c my_app -n "__fish_use_subcommand" -l choice -r -f -a "bash fish zsh" +complete -c my_app -n "__fish_use_subcommand" -l unknown -r +complete -c my_app -n "__fish_use_subcommand" -l other -r -f +complete -c my_app -n "__fish_use_subcommand" -s p -l path -r -F +complete -c my_app -n "__fish_use_subcommand" -s f -l file -r -F +complete -c my_app -n "__fish_use_subcommand" -s d -l dir -r -f -a "(__fish_complete_directories)" +complete -c my_app -n "__fish_use_subcommand" -s e -l exe -r -F +complete -c my_app -n "__fish_use_subcommand" -l cmd-name -r -f -a "(__fish_complete_command)" +complete -c my_app -n "__fish_use_subcommand" -s c -l cmd -r -f -a "(__fish_complete_command)" +complete -c my_app -n "__fish_use_subcommand" -s u -l user -r -f -a "(__fish_complete_users)" +complete -c my_app -n "__fish_use_subcommand" -s h -l host -r -f -a "(__fish_print_hostnames)" +complete -c my_app -n "__fish_use_subcommand" -l url -r -f +complete -c my_app -n "__fish_use_subcommand" -l email -r -f +complete -c my_app -n "__fish_use_subcommand" -l help -d 'Prints help information' +"#; + +#[test] +fn zsh_with_value_hints() { + let mut app = build_app_with_value_hints(); + common::(&mut app, "my_app", ZSH_VALUE_HINTS); +} + +#[test] +fn fish_with_value_hints() { + let mut app = build_app_with_value_hints(); + common::(&mut app, "my_app", FISH_VALUE_HINTS); +} diff --git a/src/build/app/mod.rs b/src/build/app/mod.rs index 8681d3a99cd..6e5c38febb7 100644 --- a/src/build/app/mod.rs +++ b/src/build/app/mod.rs @@ -26,7 +26,7 @@ use crate::{ output::{fmt::Colorizer, Help, HelpWriter, Usage}, parse::{ArgMatcher, ArgMatches, Input, Parser}, util::{safe_exit, termcolor::ColorChoice, ArgStr, Id, Key}, - Result as ClapResult, INTERNAL_ERROR_MSG, + Result as ClapResult, ValueHint, INTERNAL_ERROR_MSG, }; // FIXME (@CreepySkeleton): some of these variants are never constructed @@ -2249,6 +2249,20 @@ impl<'help> App<'help> { "Argument '{}' has both `validator` and `validator_os` set which is not allowed", arg.name ); + + if arg.value_hint == ValueHint::CommandWithArguments { + assert!( + arg.short.is_none() && arg.long.is_none(), + "Argument '{}' has hint CommandWithArguments and must be positional.", + arg.name + ); + + assert!( + self.is_set(AppSettings::TrailingVarArg), + "Positional argument '{}' has hint CommandWithArguments, so App must have TrailingVarArg set.", + arg.name + ); + } } for group in &self.groups { diff --git a/src/build/arg/mod.rs b/src/build/arg/mod.rs index 7dfa6312519..828487da1ae 100644 --- a/src/build/arg/mod.rs +++ b/src/build/arg/mod.rs @@ -1,8 +1,10 @@ mod settings; #[cfg(test)] mod tests; +mod value_hint; pub use self::settings::ArgSettings; +pub use self::value_hint::ValueHint; // Std use std::{ @@ -90,6 +92,7 @@ pub struct Arg<'help> { pub(crate) help_heading: Option<&'help str>, pub(crate) global: bool, pub(crate) exclusive: bool, + pub(crate) value_hint: ValueHint, } /// Getters @@ -155,6 +158,11 @@ impl<'help> Arg<'help> { pub fn get_index(&self) -> Option { self.index } + + /// Get the value hint of this argument + pub fn get_value_hint(&self) -> ValueHint { + self.value_hint + } } impl<'help> Arg<'help> { @@ -4099,6 +4107,38 @@ impl<'help> Arg<'help> { self } + /// Sets a hint about the type of the value for shell completions + /// + /// Currently this is only supported by the zsh completions generator. + /// + /// For example, to take a username as argument: + /// ``` + /// # use clap::{Arg, ValueHint}; + /// Arg::new("user") + /// .short('u') + /// .long("user") + /// .value_hint(ValueHint::Username) + /// # ; + /// ``` + /// + /// To take a full command line and its arguments (for example, when writing a command wrapper): + /// ``` + /// # use clap::{App, AppSettings, Arg, ValueHint}; + /// App::new("prog") + /// .setting(AppSettings::TrailingVarArg) + /// .arg( + /// Arg::new("command") + /// .multiple(true) + /// .value_hint(ValueHint::CommandWithArguments) + /// ) + /// # ; + /// ``` + pub fn value_hint(mut self, value_hint: ValueHint) -> Self { + self.set_mut(ArgSettings::TakesValue); + self.value_hint = value_hint; + self + } + // FIXME: (@CreepySkeleton) #[doc(hidden)] pub fn _build(&mut self) { @@ -4196,6 +4236,21 @@ impl Arg<'_> { "Argument '{}' cannot conflict with itself", self.name, ); + + if self.value_hint != ValueHint::Unknown { + assert!( + self.is_set(ArgSettings::TakesValue), + "Argument '{}' has value hint but takes no value", + self.name + ); + + if self.value_hint == ValueHint::CommandWithArguments { + assert!( + self.is_set(ArgSettings::MultipleValues), + "Argument '{}' uses hint CommandWithArguments and must accept multiple values", + ) + } + } } } @@ -4263,6 +4318,7 @@ impl<'help> From<&'help Yaml> for Arg<'help> { "requires_ifs" => yaml_tuple2!(a, v, requires_if), "conflicts_with" => yaml_vec_or_str!(v, a, conflicts_with), "exclusive" => yaml_to_bool!(a, v, exclusive), + "value_hint" => yaml_str_parse!(a, v, value_hint), "hide_default_value" => yaml_to_bool!(a, v, hide_default_value), "overrides_with" => yaml_vec_or_str!(v, a, overrides_with), "possible_values" => yaml_vec_or_str!(v, a, possible_value), @@ -4453,6 +4509,7 @@ impl<'help> fmt::Debug for Arg<'help> { .field("help_heading", &self.help_heading) .field("global", &self.global) .field("exclusive", &self.exclusive) + .field("value_hint", &self.value_hint) .field("default_missing_vals", &self.default_missing_vals) .finish() } diff --git a/src/build/arg/value_hint.rs b/src/build/arg/value_hint.rs new file mode 100644 index 00000000000..087712bdd59 --- /dev/null +++ b/src/build/arg/value_hint.rs @@ -0,0 +1,92 @@ +use std::str::FromStr; + +/// Provides hints about argument types for shell command completion. +/// +/// See the `clap_generate` crate for completion script generation. +/// +/// Overview of which hints are supported by which shell: +/// +/// | Hint | zsh | fish[^1]| +/// | ---------------------- | --- | ------- | +/// | `AnyPath` | Yes | Yes | +/// | `FilePath` | Yes | Yes | +/// | `DirPath` | Yes | Yes | +/// | `ExecutablePath` | Yes | Partial | +/// | `CommandName` | Yes | Yes | +/// | `CommandString` | Yes | Partial | +/// | `CommandWithArguments` | Yes | | +/// | `Username` | Yes | Yes | +/// | `Hostname` | Yes | Yes | +/// | `Url` | Yes | | +/// | `EmailAddress` | Yes | | +/// +/// [^1]: fish completions currently only support named arguments (e.g. -o or --opt), not +/// positional arguments. +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum ValueHint { + /// Default value if hint is not specified. Follows shell default behavior, which is usually + /// auto-completing filenames. + Unknown, + /// None of the hints below apply. Disables shell completion for this argument. + Other, + /// Any existing path. + AnyPath, + /// Path to a file. + FilePath, + /// Path to a directory. + DirPath, + /// Path to an executable file. + ExecutablePath, + /// Name of a command, without arguments. May be relative to PATH, or full path to executable. + CommandName, + /// A single string containing a command and its arguments. + CommandString, + /// Capture the remaining arguments as a command name and arguments for that command. This is + /// common when writing shell wrappers that execute anther command, for example `sudo` or `env`. + /// + /// This hint is special, the argument must be a positional argument and have + /// [`.multiple(true)`] and App must use [`AppSettings::TrailingVarArg`]. The result is that the + /// command line `my_app ls -la /` will be parsed as `["ls", "-la", "/"]` and clap won't try to + /// parse the `-la` argument itself. + /// + /// [`.multiple(true)`]: ./struct.Arg.html#method.multiple + /// [`AppSettings::TrailingVarArg`]: ./enum.AppSettings.html#variant.TrailingVarArg + CommandWithArguments, + /// Name of a local operating system user. + Username, + /// Host name of a computer. + /// Shells usually parse `/etc/hosts` and `.ssh/known_hosts` to complete hostnames. + Hostname, + /// Complete web address. + Url, + /// Email address. + EmailAddress, +} + +impl Default for ValueHint { + fn default() -> Self { + ValueHint::Unknown + } +} + +impl FromStr for ValueHint { + type Err = String; + fn from_str(s: &str) -> Result::Err> { + Ok(match &*s.to_ascii_lowercase() { + "unknown" => ValueHint::Unknown, + "other" => ValueHint::Other, + "anypath" => ValueHint::AnyPath, + "filepath" => ValueHint::FilePath, + "dirpath" => ValueHint::DirPath, + "executablepath" => ValueHint::ExecutablePath, + "commandname" => ValueHint::CommandName, + "commandstring" => ValueHint::CommandString, + "commandwitharguments" => ValueHint::CommandWithArguments, + "username" => ValueHint::Username, + "hostname" => ValueHint::Hostname, + "url" => ValueHint::Url, + "emailaddress" => ValueHint::EmailAddress, + _ => return Err(format!("unknown ValueHint: `{}`", s)), + }) + } +} diff --git a/src/build/macros.rs b/src/build/macros.rs index a4c2f8feb77..5840d6681bf 100644 --- a/src/build/macros.rs +++ b/src/build/macros.rs @@ -117,6 +117,17 @@ macro_rules! yaml_str { }}; } +#[cfg(feature = "yaml")] +macro_rules! yaml_str_parse { + ($a:ident, $v:ident, $c:ident) => {{ + $a.$c($v + .as_str() + .unwrap_or_else(|| panic!("failed to convert YAML {:?} value to a string", $v)) + .parse() + .unwrap_or_else(|err| panic!("{}", err))) + }}; +} + #[cfg(feature = "yaml")] macro_rules! yaml_to_char { ($a:ident, $v:ident, $c:ident) => {{ diff --git a/src/build/mod.rs b/src/build/mod.rs index 9ed76dacb2c..4006a9fce63 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -9,6 +9,6 @@ mod usage_parser; pub use self::{ app::{App, AppSettings}, - arg::{Arg, ArgSettings}, + arg::{Arg, ArgSettings, ValueHint}, arg_group::ArgGroup, }; diff --git a/src/lib.rs b/src/lib.rs index 7c628cfaed8..2cf50f5558a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,7 @@ compile_error!("`std` feature is currently required to build `clap`"); pub use crate::{ - build::{App, AppSettings, Arg, ArgGroup, ArgSettings}, + build::{App, AppSettings, Arg, ArgGroup, ArgSettings, ValueHint}, derive::{ArgEnum, Clap, FromArgMatches, IntoApp, Subcommand}, parse::errors::{Error, ErrorKind, Result}, parse::{ArgMatches, Indices, OsValues, Values}, diff --git a/tests/fixtures/app.yaml b/tests/fixtures/app.yaml index c4cfd28569a..feef7c99c40 100644 --- a/tests/fixtures/app.yaml +++ b/tests/fixtures/app.yaml @@ -112,6 +112,10 @@ args: help: Test case_insensitive possible_values: [test123, test321] case_insensitive: true + - value_hint: + long: value-hint + help: Test value_hint + value_hint: FilePath arg_groups: - test: diff --git a/tests/yaml.rs b/tests/yaml.rs index d19a3efc199..12b3affd5f9 100644 --- a/tests/yaml.rs +++ b/tests/yaml.rs @@ -1,6 +1,6 @@ #![cfg(feature = "yaml")] -use clap::{load_yaml, App}; +use clap::{load_yaml, App, ValueHint}; #[test] fn create_app_from_yaml() { @@ -41,3 +41,16 @@ fn author() { let help_string = String::from_utf8(help_buffer).unwrap(); assert!(help_string.contains("Kevin K. ")); } + +// ValueHint must be parsed correctly from Yaml +#[test] +fn value_hint() { + let yml = load_yaml!("fixtures/app.yaml"); + let app = App::from(yml); + + let arg = app + .get_arguments() + .find(|a| a.get_name() == "value_hint") + .unwrap(); + assert_eq!(arg.get_value_hint(), ValueHint::FilePath); +}