Repository: clap-rs/clap (language: rust)

Issue:
clap_complete: File path is completed too early in bash
### Please complete the following tasks

- [X] I have searched the [discussions](https://github.com/clap-rs/clap/discussions)
- [X] I have searched the [open](https://github.com/clap-rs/clap/issues) and [rejected](https://github.com/clap-rs/clap/issues?q=is%3Aissue+label%3AS-wont-fix+is%3Aclosed) issues

### Rust Version

rustc 1.74.0 (79e9716c9 2023-11-13)

### Clap Version

clap 4.4.10 (clap_complete 4.4.4)

### Minimal reproducible code

build.rs

```rust
use clap::{CommandFactory, Parser, ValueHint};
use clap_complete::Shell;
use std::env;
use std::path::PathBuf;

#[derive(Parser)]
struct Args {
    #[arg(long, value_hint = ValueHint::FilePath)]
    file: PathBuf,
}

fn main() {
    let mut cmd = Args::command();
    let out_dir = env::var("OUT_DIR").unwrap();
    clap_complete::generate_to(Shell::Bash, &mut cmd, "clap_path_compl", out_dir).unwrap();
}
```

### Steps to reproduce the bug with the above code

```sh
cargo new clap_path_compl
cd clap_path_compl
cargo add clap --build -F derive
cargo add --build clap_complete
vim build.rs # Enter the code as above
cargo build
source target/debug/build/clap_path_compl-*/out/clap_path_compl.bash
```

Enter `./target/debug/clap_path_compl --file ` and press TAB to start completion.

### Actual Behaviour

```
# If user enters as follows:
./target/debug/clap_path_compl --file /us<TAB>

# `/usr ` is completed and move to next argument
./target/debug/clap_path_compl --file /usr 
```


### Expected Behaviour

```
# If user enters as follows:
./target/debug/clap_path_compl --file /us<TAB>

# `/usr/` should be completed
# because `/usr` is a directory, not a file.
./target/debug/clap_path_compl --file /usr/
```


### Additional Context

A possible solution is to use `compopt`.

This issue can be fixed by patching out the completion script as follows:

```diff
--- target/debug/build/clap_path_compl-55b9c400b2155aaf/out/clap_path_compl.bash.orig
+++ target/debug/build/clap_path_compl-55b9c400b2155aaf/out/clap_path_compl.bash
@@ -27,6 +27,7 @@
             case "${prev}" in
                 --file)
                     COMPREPLY=($(compgen -f "${cur}"))
+                    compopt -o filenames
                     return 0
                     ;;
                 *)
```

### Debug Output

_No response_


Discussion / hints:
By the way, I think it's possible to improve support of other variants of ValueHint with `compopt`, like `-o plusdirs` for `ValueHint::DirPath` or `-o nospace` for `ValueHint::Other`.

Fix patch:
diff --git a/clap_complete/src/shells/bash.rs b/clap_complete/src/shells/bash.rs
index 726078ab366..cc9ce2a874e 100644
--- a/clap_complete/src/shells/bash.rs
+++ b/clap_complete/src/shells/bash.rs
@@ -168,29 +168,50 @@ fn option_details_for_path(cmd: &Command, path: &str) -> String {
     let mut opts = vec![String::new()];
 
     for o in p.get_opts() {
+        let compopt = match o.get_value_hint() {
+            ValueHint::FilePath => Some("compopt -o filenames"),
+            ValueHint::DirPath => Some("compopt -o plusdirs"),
+            ValueHint::Other => Some("compopt -o nospace"),
+            _ => None,
+        };
+
         if let Some(longs) = o.get_long_and_visible_aliases() {
             opts.extend(longs.iter().map(|long| {
-                format!(
-                    "--{})
-                    COMPREPLY=({})
-                    return 0
-                    ;;",
-                    long,
-                    vals_for(o)
-                )
+                let mut v = vec![
+                    format!("--{})", long),
+                    format!("COMPREPLY=({})", vals_for(o)),
+                ];
+
+                if let Some(copt) = compopt {
+                    v.extend([
+                        r#"if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then"#.to_string(),
+                        format!("    {}", copt),
+                        "fi".to_string(),
+                    ]);
+                }
+
+                v.extend(["return 0", ";;"].iter().map(|s| s.to_string()));
+                v.join("\n                    ")
             }));
         }
 
         if let Some(shorts) = o.get_short_and_visible_aliases() {
             opts.extend(shorts.iter().map(|short| {
-                format!(
-                    "-{})
-                    COMPREPLY=({})
-                    return 0
-                    ;;",
-                    short,
-                    vals_for(o)
-                )
+                let mut v = vec![
+                    format!("-{})", short),
+                    format!("COMPREPLY=({})", vals_for(o)),
+                ];
+
+                if let Some(copt) = compopt {
+                    v.extend([
+                        r#"if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then"#.to_string(),
+                        format!("    {}", copt),
+                        "fi".to_string(),
+                    ]);
+                }
+
+                v.extend(["return 0", ";;"].iter().map(|s| s.to_string()));
+                v.join("\n                    ")
             }));
         }
     }
@@ -210,6 +231,8 @@ fn vals_for(o: &Arg) -> String {
                 .collect::<Vec<_>>()
                 .join(" ")
         )
+    } else if o.get_value_hint() == ValueHint::DirPath {
+        String::from("") // should be empty to avoid duplicate candidates
     } else if o.get_value_hint() == ValueHint::Other {
         String::from("\"${cur}\"")
     } else {


Test patch:
diff --git a/clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc b/clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc
index 490fcbf3c1d..2f72ccf40b2 100644
--- a/clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc
+++ b/clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc
@@ -542,6 +542,9 @@ _exhaustive() {
                     ;;
                 --other)
                     COMPREPLY=("${cur}")
+                    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
+                        compopt -o nospace
+                    fi
                     return 0
                     ;;
                 --path)
@@ -554,18 +557,30 @@ _exhaustive() {
                     ;;
                 --file)
                     COMPREPLY=($(compgen -f "${cur}"))
+                    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
+                        compopt -o filenames
+                    fi
                     return 0
                     ;;
                 -f)
                     COMPREPLY=($(compgen -f "${cur}"))
+                    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
+                        compopt -o filenames
+                    fi
                     return 0
                     ;;
                 --dir)
-                    COMPREPLY=($(compgen -f "${cur}"))
+                    COMPREPLY=()
+                    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
+                        compopt -o plusdirs
+                    fi
                     return 0
                     ;;
                 -d)
-                    COMPREPLY=($(compgen -f "${cur}"))
+                    COMPREPLY=()
+                    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
+                        compopt -o plusdirs
+                    fi
                     return 0
                     ;;
                 --exe)
diff --git a/clap_complete/tests/snapshots/value_hint.bash b/clap_complete/tests/snapshots/value_hint.bash
index 61e2f61e96e..35f6b3d56b1 100644
--- a/clap_complete/tests/snapshots/value_hint.bash
+++ b/clap_complete/tests/snapshots/value_hint.bash
@@ -35,6 +35,9 @@ _my-app() {
                     ;;
                 --other)
                     COMPREPLY=("${cur}")
+                    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
+                        compopt -o nospace
+                    fi
                     return 0
                     ;;
                 --path)
@@ -47,18 +50,30 @@ _my-app() {
                     ;;
                 --file)
                     COMPREPLY=($(compgen -f "${cur}"))
+                    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
+                        compopt -o filenames
+                    fi
                     return 0
                     ;;
                 -f)
                     COMPREPLY=($(compgen -f "${cur}"))
+                    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
+                        compopt -o filenames
+                    fi
                     return 0
                     ;;
                 --dir)
-                    COMPREPLY=($(compgen -f "${cur}"))
+                    COMPREPLY=()
+                    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
+                        compopt -o plusdirs
+                    fi
                     return 0
                     ;;
                 -d)
-                    COMPREPLY=($(compgen -f "${cur}"))
+                    COMPREPLY=()
+                    if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
+                        compopt -o plusdirs
+                    fi
                     return 0
                     ;;
                 --exe)
diff --git a/clap_complete/tests/testsuite/bash.rs b/clap_complete/tests/testsuite/bash.rs
index 2e960136704..7edb458cc9d 100644
--- a/clap_complete/tests/testsuite/bash.rs
+++ b/clap_complete/tests/testsuite/bash.rs
@@ -161,6 +161,58 @@ fn complete() {
 -V          --generate  --version   quote       pacman      alias       complete    "#;
     let actual = runtime.complete(input, &term).unwrap();
     snapbox::assert_eq(expected, actual);
+
+    // Issue 5239 (https://github.com/clap-rs/clap/issues/5239)
+    let input = "exhaustive hint --file test\t";
+    let expected = "exhaustive hint --file test     % exhaustive hint --file tests/";
+    let actual = runtime.complete(input, &term).unwrap();
+    snapbox::assert_eq(expected, actual);
+
+    {
+        use std::fs::File;
+        use std::path::Path;
+
+        let testdir = snapbox::path::PathFixture::mutable_temp().unwrap();
+        let testdir_path = testdir.path().unwrap();
+
+        File::create(Path::new(testdir_path).join("a_file")).unwrap();
+        File::create(Path::new(testdir_path).join("b_file")).unwrap();
+        std::fs::create_dir(Path::new(testdir_path).join("c_dir")).unwrap();
+        std::fs::create_dir(Path::new(testdir_path).join("d_dir")).unwrap();
+
+        let input = format!(
+            "exhaustive hint --file {}/\t\t",
+            testdir_path.to_string_lossy()
+        );
+        let actual = runtime.complete(input.as_str(), &term).unwrap();
+        assert!(
+            actual.contains("a_file")
+                && actual.contains("b_file")
+                && actual.contains("c_dir")
+                && actual.contains("d_dir"),
+            "Actual output:\n{}",
+            actual
+        );
+
+        let input = format!(
+            "exhaustive hint --dir {}/\t\t",
+            testdir_path.to_string_lossy()
+        );
+        let actual = runtime.complete(input.as_str(), &term).unwrap();
+        assert!(
+            !actual.contains("a_file")
+                && !actual.contains("b_file")
+                && actual.contains("c_dir")
+                && actual.contains("d_dir"),
+            "Actual output:\n{}",
+            actual
+        );
+    }
+
+    let input = "exhaustive hint --other \t";
+    let expected = "exhaustive hint --other         % exhaustive hint --other ";
+    let actual = runtime.complete(input, &term).unwrap();
+    snapbox::assert_eq(expected, actual);
 }
 
 #[test]