Skip to content

Commit

Permalink
Add hinting of allowed value types for zsh completion
Browse files Browse the repository at this point in the history
Adds new method/attribute `Arg::value_hint`, taking a `ValueHint` enum
as argument. The hint can denote accepted values, for example: paths,
usernames, hostnames, command names, etc.

This initial implementation only supports hints for the zsh completion
generator, but support for other shells can be added later.
  • Loading branch information
intgr committed Aug 6, 2020
1 parent a4bc1f2 commit c5b4e89
Show file tree
Hide file tree
Showing 13 changed files with 562 additions and 45 deletions.
114 changes: 114 additions & 0 deletions clap_generate/examples/value_hints.rs
Original file line number Diff line number Diff line change
@@ -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 --<TAB>
//! ```
//! fish:
//! ```sh
//! cargo run --example value_hints -- --generate=fish > value_hints.fish
//! . ./value_hints.fish
//! ./target/debug/examples/value_hints --<TAB>
//! ```
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::<Bash, _>(&mut app, APPNAME, &mut io::stdout()),
"elvish" => generate::<Elvish, _>(&mut app, APPNAME, &mut io::stdout()),
"fish" => generate::<Fish, _>(&mut app, APPNAME, &mut io::stdout()),
"powershell" => generate::<PowerShell, _>(&mut app, APPNAME, &mut io::stdout()),
"zsh" => generate::<Zsh, _>(&mut app, APPNAME, &mut io::stdout()),
_ => panic!("Unknown generator"),
}
}
}
34 changes: 31 additions & 3 deletions clap_generate/src/generators/shells/fish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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()
}
}
83 changes: 51 additions & 32 deletions clap_generate/src/generators/shells/zsh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
if let Some(values) = &arg.get_possible_values() {
Some(format!(
"({})",
values
.iter()
.map(|&v| escape_value(v))
.collect::<Vec<_>>()
.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
Expand Down Expand Up @@ -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::<Vec<String>>()
.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
);

Expand All @@ -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
);

Expand All @@ -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
);

Expand Down Expand Up @@ -525,34 +553,25 @@ 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()
.map_or("".to_owned(), |v| " -- ".to_owned() + v)
.replace("[", "\\[")
.replace("]", "\\]")
.replace(":", "\\:"),
action = arg
.get_possible_values()
.map_or("_files".to_owned(), |values| {
format!(
"({})",
values
.iter()
.map(|v| escape_value(*v))
.collect::<Vec<String>>()
.join(" ")
)
})
value_completion = value_completion(arg).unwrap_or_else(|| "".to_string())
);

debug!("write_positionals_of:iter: Wrote...{}", a);
Expand Down
16 changes: 10 additions & 6 deletions clap_generate/tests/completions.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clap::{App, Arg};
use clap::{App, Arg, ValueHint};
use clap_generate::{generate, generators::*};
use std::fmt;

Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -773,7 +777,7 @@ fn build_app_special_help() -> App<'static> {
)
}

fn common<G: Generator>(app: &mut App, name: &str, fixture: &str) {
pub fn common<G: Generator>(app: &mut App, name: &str, fixture: &str) {
let mut buf = vec![];
generate::<G, _>(app, name, &mut buf);
let string = String::from_utf8(buf).unwrap();
Expand Down
Loading

0 comments on commit c5b4e89

Please sign in to comment.