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 Apr 12, 2020
1 parent 4c3b965 commit 278ba99
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 33 deletions.
107 changes: 107 additions & 0 deletions clap_generate/examples/value_hints.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//! 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::{Fish, 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::with_name("generator")
.long("generate")
.possible_values(&["bash", "fish", "zsh"]),
)
.arg(
Arg::with_name("unknown")
.long("unknown")
.value_hint(ValueHint::Unknown),
)
.arg(
Arg::with_name("path")
.long("path")
.short('p')
.value_hint(ValueHint::AnyPath),
)
.arg(
Arg::with_name("file")
.long("file")
.short('f')
.value_hint(ValueHint::FilePath),
)
.arg(
Arg::with_name("dir")
.long("dir")
.short('d')
.value_hint(ValueHint::DirPath),
)
.arg(
Arg::with_name("exe")
.long("exe")
.short('e')
.value_hint(ValueHint::ExecutablePath),
)
.arg(
Arg::with_name("cmd_name")
.long("cmd-name")
.value_hint(ValueHint::CommandName),
)
.arg(
Arg::with_name("cmd")
.long("cmd")
.short('c')
.value_hint(ValueHint::CommandString),
)
.arg(
Arg::with_name("command_with_args").multiple(true), // .value_hint(ValueHint::CommandWithArguments),
)
.arg(
Arg::with_name("user")
.short('u')
.long("user")
.value_hint(ValueHint::Username),
)
.arg(
Arg::with_name("host")
.short('h')
.long("host")
.value_hint(ValueHint::Hostname),
)
.arg(Arg::with_name("url").long("url").value_hint(ValueHint::Url))
.arg(
Arg::with_name("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()),
"fish" => generate::<Fish, _>(&mut app, APPNAME, &mut io::stdout()),
"zsh" => generate::<Zsh, _>(&mut app, APPNAME, &mut io::stdout()),
_ => panic!("Unknown generator"),
}
}
}
19 changes: 19 additions & 0 deletions clap_generate/src/generators/shells/fish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,25 @@ fn gen_fish_inner(root_command: &str, app: &App, buffer: &mut String) {

if let Some(ref data) = option.get_possible_values() {
template.push_str(format!(" -r -f -a \"{}\"", data.join(" ")).as_str());
} else {
assert!(option.is_set(ArgSettings::TakesValue));
let completion = match option.value_hint {
ValueHint::AnyPath | ValueHint::FilePath => "__fish_complete_path",
ValueHint::DirPath => "__fish_complete_directories",
// ValueHint::ExecutablePath => "_absolute_command_paths",
// ValueHint::CommandName => "_command_names -e",
// ValueHint::CommandString => "_cmdstring",
ValueHint::CommandWithArguments => "__fish_complete_subcommand -- -n --interval",
ValueHint::Username => "__fish_complete_users",
ValueHint::Hostname => "__fish_print_hostnames",
// ValueHint::Url => "_urls",
// ValueHint::EmailAddress => "_email_addresses",
_ => "",
};

if !completion.is_empty() {
template.push_str(format!(" -r -f -a \"({})\"", completion).as_str());
}
}

buffer.push_str(template.as_str());
Expand Down
80 changes: 49 additions & 31 deletions clap_generate/src/generators/shells/zsh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,9 @@ fn parser_of<'b>(p: &'b App<'b>, mut sc: &str) -> &'b App<'b> {
find_subcmd!(p, sc).expect(INTERNAL_ERROR_MSG)
}

// Writes out the args section, which ends up being the flags, opts and postionals, and a jump to
// Writes out the args section, which ends up being the flags, opts and positionals, and a jump to
// another ZSH function if there are subcommands.
// The structer works like this:
// The structure works like this:
// ([conflicting_args]) [multiple] arg [takes_value] [[help]] [: :(possible_values)]
// ^-- list '-v -h' ^--'*' ^--'+' ^-- list 'one two three'
//
Expand Down Expand Up @@ -331,6 +331,37 @@ 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) -> String {
if let Some(values) = &arg.get_possible_values() {
format!(
"({})",
values
.iter()
.map(|&v| escape_value(v))
.collect::<Vec<_>>()
.join(" ")
)
} else {
match arg.value_hint {
// zsh default behavior is completing filenames
ValueHint::Unknown => "_files",
ValueHint::AnyPath => "_files",
ValueHint::FilePath => "_files",
ValueHint::DirPath => "_files -/",
ValueHint::ExecutablePath => "_absolute_command_paths",
ValueHint::CommandName => "_command_names -e",
ValueHint::CommandString => "_cmdstring",
ValueHint::CommandWithArguments => "_command_names -e",
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 @@ -371,26 +402,15 @@ 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 = value_completion(o);

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 @@ -400,11 +420,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 @@ -501,7 +521,7 @@ fn write_positionals_of(p: &App) -> String {
};

let a = format!(
"'{optional}:{name}{help}:{action}' \\",
"'{optional}:{name}{help}:{value_completion}' \\",
optional = optional,
name = arg.get_name(),
help = arg
Expand All @@ -510,23 +530,21 @@ 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::<Vec<String>>()
.join(" ")
)
})
value_completion = value_completion(arg)
);

debugln!("Zsh::write_positionals_of:iter: Wrote...{}", a);

ret.push(a);

// Special case for multi-argument commands
if arg.value_hint == ValueHint::CommandWithArguments
&& arg.is_set(ArgSettings::MultipleValues)
{
let a = "'*::arguments:_normal' \\".to_string();
debugln!("Zsh::write_positionals_of:iter: Wrote...{}", a);
ret.push(a);
}
}

ret.join("\n")
Expand Down
37 changes: 37 additions & 0 deletions src/build/arg/mod.rs
Original file line number Diff line number Diff line change
@@ -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::borrow::Cow;
Expand Down Expand Up @@ -87,6 +89,8 @@ pub struct Arg<'help> {
pub(crate) help_heading: Option<&'help str>,
pub(crate) global: bool,
pub(crate) exclusive: bool,
#[doc(hidden)]
pub value_hint: ValueHint,
}

/// Getters
Expand Down Expand Up @@ -4068,6 +4072,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::with_name("user")
/// .short('u')
/// .long("user")
/// .value_hint(ValueHint::Username)
/// # ;
/// ```
///
/// To take a full command line and its arguments (when writing a command wrapper):
/// ```
/// # use clap::{App, AppSettings, Arg, ValueHint};
/// App::new("prog")
/// .setting(AppSettings::TrailingVarArg)
/// .arg(
/// Arg::with_name("command")
/// .multiple(true)
/// .value_hint(ValueHint::CommandWithArguments)
/// )
/// # ;
/// ```
pub fn value_hint(mut self, value_hint: ValueHint) -> Self {
self.setb(ArgSettings::TakesValue);
self.value_hint = value_hint;
self
}

// FIXME: (@CreepySkeleton)
#[doc(hidden)]
pub fn _build(&mut self) {
Expand Down Expand Up @@ -4304,6 +4340,7 @@ impl<'help> Eq for Arg<'help> {}

impl<'help> fmt::Debug for Arg<'help> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
// TODO!
write!(
f,
"Arg {{ id: {:X?}, name: {:?}, help: {:?}, long_help: {:?}, conflicts_with: {:?}, \
Expand Down
41 changes: 41 additions & 0 deletions src/build/arg/value_hint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/// Provides hints for shell completion.
/// TODO expand documentation
/// TODO more options?
/// TODO serialization/yaml/etc?
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum ValueHint {
/// Default value if hint is not specified
Unknown,
/// Any existing path
AnyPath,
/// Path to a file
FilePath,
/// Path to a directory
DirPath,
/// Path to an executable file
ExecutablePath,
/// Command name, relative to a directory in PATH, or full path to executable
CommandName,
/// A single string containing a command and its arguments
CommandString,
/// A command and each argument as multiple strings. Common for command wrappers.
///
/// This hint is special, the argument must be a positional argument and use `.multiple(true)`.
/// It's also recommended to use `AppSettings::TrailingVarArg` to avoid confusion about which
/// command the following options belong to.
CommandWithArguments,
/// Name of an operating system user
Username,
/// Host name of a computer
Hostname,
/// Complete web address
Url,
/// Email address
EmailAddress,
}

impl Default for ValueHint {
fn default() -> Self {
ValueHint::Unknown
}
}
2 changes: 1 addition & 1 deletion src/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ mod arg_group;
mod usage_parser;

pub use self::app::{App, AppSettings};
pub use self::arg::{Arg, ArgSettings};
pub use self::arg::{Arg, ArgSettings, ValueHint};
pub use self::arg_group::ArgGroup;
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@
#[cfg(not(feature = "std"))]
compile_error!("`std` feature is currently required to build `clap`");

pub use crate::build::{App, AppSettings, Arg, ArgGroup, ArgSettings};
pub use crate::build::{App, AppSettings, Arg, ArgGroup, ArgSettings, ValueHint};
pub use crate::derive::{Clap, FromArgMatches, IntoApp, Subcommand};
pub use crate::parse::errors::{Error, ErrorKind, Result};
pub use crate::parse::{ArgMatches, OsValues, SubCommand, Values};
Expand Down

0 comments on commit 278ba99

Please sign in to comment.