Repository: clap-rs/clap
Pull request #1793 (2020-04-07 23:42:57)

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.

intgr: Still needs some polish, but I wanted to open a PR to get some feedback first.
pksunkara: Looks like a great start. Appreciate this @intgr 

Btw, could you help us test if https://github.com/clap-rs/clap/issues/850 has been fixed or not? I couldn't get it working on my local zsh
pickfire: Is it possible to extract the type of data to do value hinting automatically in addition to this, like making use of rust types like `File` will get a file? I think this should be harder but helps reduces user effort, of course the limitation here it that we can't figure out if we we want a file or a host from rust type itself.
intgr: > Is it possible to extract the type of data to do value hinting automatically

Good idea, I will look into it.

pksunkara: Also, can we do something about `bash` and `fish` too? I could try my hand at them later, but it would be great if we can support them in this PR. 
intgr: > Also, can we do something about bash and fish too? I could try my hand at them later, but it would be great if we can support them in this PR.

Honestly, I'd prefer to keep the scope of this PR as small as possible. This is my first contribution to clap, I'm still learning Rust, that's already lots of new things. I haven't used bash for years, never even tried fish.
Dylan-DPC: @intgr agree, go on don't worry. we can discuss that in a separate issue/pr
CreepySkeleton: > Is it possible to extract the type of data to do value hinting automatically

I doubt this is possible with raw clap since it's "stringy typed", no way to get type information. This _might_ be possible with derive but it would be a very simple heuristic (is it path or not). This hint is not a big burden, anyway.
CreepySkeleton: > Also, can we do something about bash and fish too? I could try my hand at them later, but it would be great if we can support them in this PR.

I can try to improve `bash` later. no experience with fish, and I'm not really interested.
pickfire: I use fish here but I don't seemed to see something equivalent (maybe I don't have much experience with completions), IIRC fish could use custom command to do these kind of completion hinting and there is no limit on what could be hint, this is both good news (it is very flexible since we can just inject some code) and bad news (there is just too many things we can hint) for us.
benjumanji: > I use fish here but I don't seemed to see something equivalent

As is the usual way with fish, rather than create lots of specialized functionality fish simply uses the same tools as the end user to provide useful functionality. 

https://fishshell.com/docs/current/index.html#useful-functions-for-writing-completions
pksunkara: Hey, sorry about this, but you should probably rebase on master since we touched clap_generate this week.
intgr: @benjumanji Actually yeah, I looked into fish already on Thursday and whipped up some rudimentary support. It's surprisingly sane compared to bash and zsh completion definitions.
intgr: Yeah this is still WIP and not entirely ready for a review yet. Meanwhile, some points for bikeshedding:

1. CreepySkeleton suggested that `ValueHint::CommandWithArguments` should automatically set `AppSettings::TrailingVarArg` and `multiple()`, which would make hints directly affect command parsing as well. So it seems it's not just a "hint" any more.

   Any suggestions what to rename `ValueHint` to? `CompletionType` seems bad for the same reason, it doesn't just affect completion. `ValueKind`? ...

2. I'm not happy with the naming of `ValueHint::CommandString` (one string containing a whole shell command) and `ValueHint::CommandWithArguments` (command and arguments as separate arguments), suggestions welcome.

CreepySkeleton: 1. Right, but allowing completions to disagree with actual implementation is also not very good. Well, this is not *necessarily* bad, we may try to sell it as "Help the generator but remember: enforcing correctness is up to you".
intgr: Yeah it makes sense, I'm just thinking if I should stop calling it a "hint" now.
pksunkara: Or, we could keep it as a hint, but add an assert that any arg hinted as `ValueHint::CommandWithArguments` in inside an app with setting `TrailingVarArg`. I recently made asserts easier to implement with https://github.com/clap-rs/clap/pull/1797
CreepySkeleton: I'd go for `ValueKind` and extensive documentation.
benjumanji: I know my opinion means nothing here, but @intgr I'm am so _excited_ for this to land! I have a new cli project that I kicked off last night and was going to pause it to work on this but you have basically already implemented everything I need.
intgr: @benjumanji I definitely see the value in custom completion functions or some other "escape hatch" mechanism, but it seems more complicated, less portable between shells and not something I need myself right now.

However, it's worth thinking ahead to what that API would look like, to make sure I'm not painting clap into a corner somehow with my current work. Personally I can't imagine way they would interact, apart from completion generators which would prefer the custom completion over `ValueHint` if there is one.
benjumanji: My naive solution would be be `ValueHint::Custom(fish: Option<String>, zsh: Option<String>, bash: Option<String>)` and just drop the string into the completion slot if it exists, use some default if it doesn't. In any case, I don't want to be strawing up the path to this landing, I can play with my ideas once this is in.
CreepySkeleton: @intgr Do you plan to finish this off? We would _love_ to see it's landed! If you need any help from us, feel free to ask.
intgr: Hey, yes I do want to finish this but I've been procrastinating. I appreciate the reminder.

Will try to make an effort soon but can't give any promises :)
intgr: My latest thinking was to follow @pksunkara 's idea: instead of changing argument's behavior when `value_hint()` is called, I would add checks to `_debug_asserts()` to make sure the hint matches arg's settings. This seems to be the approach taken for other cases as well. And it prevents changing the Arg in an incompatible manner later after setting `value_hint`.

In that case, I slightly prefer the name `ValueHint` although I would be fine with `ValueKind` as well.
intgr: By the way, do completion test snapshots (e.g. `static ZSH_SPECIAL_CMDS: &str` in `clap_generate/tests/completions.rs`) have to be updated by hand or is there an easier way?
pksunkara: No easier way for now :(
CreepySkeleton: @intgr knock knock
intgr: Hey people. I'm on vacation now and hopefully with enough motivation to move this forward. Sorry for the wait. :)

One part of this MR that I have postponed is the YAML (de)serialization part, if anyone can give me some quick pointers -- where to start -- that would be great.

CreepySkeleton: > One part of this MR that I have postponed is the YAML (de)serialization part, if anyone can give me some quick pointers -- where to start -- that would be great.

Just implement `FromStr` for it (case insensitive) and then use `yaml_str!` as it's used in [thereabouts](https://github.com/clap-rs/clap/blob/48d308a8ab9e96d4b21336e428feee420dbac51d/src/build/arg/mod.rs#L4208).
intgr: I ended up creating a new `yaml_str_parse!()` macro because the `yaml_str!` does not call `parse()` on the value. Or should I just inline the code instead of creating a new macro?

btw I also found it confusing that there are 2 different macros with the name `yaml_str!`
intgr: I think the initial feature set is now be complete, feel free to start reviewing.

There are issues with the fish generator, which already existed before my changes:
* It does not handle positional arguments at all -- only named arguments.
* There are some odd completion issues with `examples/value_hints.rs`, which disappear if I remove the `-n __fish_use_subcommand`. It seems the generator shouldn't add it if the App doesn't use subcommands at all?

I'm not very interested in fixing these issues in the fish generator. Does that seem OK? Should I keep my fish changes in or back them out from this PR? I believe my changes don't make anything worse, but it's easier to notice these problems now.

intgr: I also added a support matrix table to `ValueHint` documentation:

| Hint                   | zsh | fish    |
| ---------------------- | --- | ------- |
| `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 |         |
intgr: Ping? Rebased to latest master.

Patch:
diff --git a/clap_generate/examples/value_hints.rs b/clap_generate/examples/value_hints.rs
new file mode 100644
index 00000000..33f3ef3f
--- /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 --<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"),
+        }
+    }
+}
diff --git a/clap_generate/src/generators/shells/fish.rs b/clap_generate/src/generators/shells/fish.rs
index d02f9d66..82322462 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 c8e615c6..e32303d6 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<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
@@ -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
             );
 
@@ -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::<Vec<String>>()
-                            .join(" ")
-                    )
-                })
+            value_completion = value_completion(arg).unwrap_or_else(|| "".to_string())
         );
 
         debug!("write_positionals_of:iter: Wrote...{}", a);
diff --git a/src/build/app/mod.rs b/src/build/app/mod.rs
index 8681d3a9..6e5c38fe 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 7dfa6312..828487da 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<u64> {
         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 00000000..087712bd
--- /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<Self, <Self as FromStr>::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 a4c2f8fe..5840d668 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 9ed76dac..4006a9fc 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 7c628cfa..2cf50f55 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},


Test patch:
diff --git a/clap_generate/tests/completions.rs b/clap_generate/tests/completions.rs
index 8135ddf6..1aa76b31 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<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();
diff --git a/clap_generate/tests/value_hints.rs b/clap_generate/tests/value_hints.rs
new file mode 100644
index 00000000..83d55f30
--- /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::<Zsh>(&mut app, "my_app", ZSH_VALUE_HINTS);
+}
+
+#[test]
+fn fish_with_value_hints() {
+    let mut app = build_app_with_value_hints();
+    common::<Fish>(&mut app, "my_app", FISH_VALUE_HINTS);
+}
diff --git a/tests/fixtures/app.yaml b/tests/fixtures/app.yaml
index c4cfd285..feef7c99 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 d19a3efc..12b3affd 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. <kbknapp@gmail.com>"));
 }
+
+// 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);
+}