Repository: clap-rs/clap
Pull request #5647 (2024-08-09 21:00:14)

feat(complete): Infer shell for dynamic completions
<!--
Thanks for helping out!

Please link the appropriate issue from your PR.

If you don't have an issue, we'd recommend starting with one first so the PR can focus on the
implementation (unless its an obvious bug or documentation fix that will have
little conversation).
-->



Patch:
diff --git a/clap_complete/src/dynamic/shells/shell.rs b/clap_complete/src/dynamic/shells/shell.rs
index 874589f8..7b2598ba 100644
--- a/clap_complete/src/dynamic/shells/shell.rs
+++ b/clap_complete/src/dynamic/shells/shell.rs
@@ -10,14 +10,77 @@ use clap::ValueEnum;
 pub enum Shell {
     /// Bourne Again `SHell` (bash)
     Bash,
+    /// Elvish shell
+    Elvish,
     /// Friendly Interactive `SHell` (fish)
     Fish,
-    /// Z shell (zsh)
-    Zsh,
-    /// Elf `SHell` (elvish)
-    Elvish,
-    /// Powerful `SHell` (powershel)
+    /// `PowerShell`
     Powershell,
+    /// Z `SHell` (zsh)
+    Zsh,
+}
+
+impl Shell {
+    /// Parse a shell from a path to the executable for the shell
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use clap_complete::shells::Shell;
+    ///
+    /// assert_eq!(Shell::from_shell_path("/bin/bash"), Some(Shell::Bash));
+    /// assert_eq!(Shell::from_shell_path("/usr/bin/zsh"), Some(Shell::Zsh));
+    /// assert_eq!(Shell::from_shell_path("/opt/my_custom_shell"), None);
+    /// ```
+    pub fn from_shell_path(path: impl AsRef<std::path::Path>) -> Option<Shell> {
+        parse_shell_from_path(path.as_ref())
+    }
+
+    /// Determine the user's current shell from the environment
+    ///
+    /// This will read the SHELL environment variable and try to determine which shell is in use
+    /// from that.
+    ///
+    /// If SHELL is not set, then on windows, it will default to powershell, and on
+    /// other operating systems it will return `None`.
+    ///
+    /// If SHELL is set, but contains a value that doesn't correspond to one of the supported shell
+    /// types, then return `None`.
+    ///
+    /// # Example:
+    ///
+    /// ```no_run
+    /// # use clap::Command;
+    /// use clap_complete::{generate, shells::Shell};
+    /// # fn build_cli() -> Command {
+    /// #     Command::new("compl")
+    /// # }
+    /// let shell = Shell::from_env();
+    /// println!("{shell:?}");
+    /// ```
+    pub fn from_env() -> Option<Shell> {
+        if let Some(env_shell) = std::env::var_os("SHELL") {
+            Shell::from_shell_path(env_shell)
+        } else if cfg!(windows) {
+            Some(Shell::Powershell)
+        } else {
+            None
+        }
+    }
+}
+
+// use a separate function to avoid having to monomorphize the entire function due
+// to from_shell_path being generic
+fn parse_shell_from_path(path: &std::path::Path) -> Option<Shell> {
+    let name = path.file_stem()?.to_str()?;
+    match name {
+        "bash" => Some(Shell::Bash),
+        "elvish" => Some(Shell::Elvish),
+        "fish" => Some(Shell::Fish),
+        "powershell" | "powershell_ise" => Some(Shell::Powershell),
+        "zsh" => Some(Shell::Zsh),
+        _ => None,
+    }
 }
 
 impl Display for Shell {
@@ -47,20 +110,20 @@ impl ValueEnum for Shell {
     fn value_variants<'a>() -> &'a [Self] {
         &[
             Shell::Bash,
-            Shell::Fish,
-            Shell::Zsh,
             Shell::Elvish,
+            Shell::Fish,
             Shell::Powershell,
+            Shell::Zsh,
         ]
     }
 
     fn to_possible_value(&self) -> Option<PossibleValue> {
         Some(match self {
             Shell::Bash => PossibleValue::new("bash"),
-            Shell::Fish => PossibleValue::new("fish"),
-            Shell::Zsh => PossibleValue::new("zsh"),
             Shell::Elvish => PossibleValue::new("elvish"),
+            Shell::Fish => PossibleValue::new("fish"),
             Shell::Powershell => PossibleValue::new("powershell"),
+            Shell::Zsh => PossibleValue::new("zsh"),
         })
     }
 }
@@ -69,10 +132,10 @@ impl Shell {
     fn completer(&self) -> &dyn crate::dynamic::shells::ShellCompleter {
         match self {
             Self::Bash => &super::Bash,
-            Self::Fish => &super::Fish,
-            Self::Zsh => &super::Zsh,
             Self::Elvish => &super::Elvish,
+            Self::Fish => &super::Fish,
             Self::Powershell => &super::Powershell,
+            Self::Zsh => &super::Zsh,
         }
     }
 }


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 e6dde351..49c9960b 100644
--- a/clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc
+++ b/clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc
@@ -245,7 +245,7 @@ _exhaustive() {
             fi
             case "${prev}" in
                 --shell)
-                    COMPREPLY=($(compgen -W "bash fish zsh elvish powershell" -- "${cur}"))
+                    COMPREPLY=($(compgen -W "bash elvish fish powershell zsh" -- "${cur}"))
                     return 0
                     ;;
                 --register)
diff --git a/clap_complete/tests/snapshots/home/static/exhaustive/fish/fish/completions/exhaustive.fish b/clap_complete/tests/snapshots/home/static/exhaustive/fish/fish/completions/exhaustive.fish
index 1b4b9bd7..7b28a7cd 100644
--- a/clap_complete/tests/snapshots/home/static/exhaustive/fish/fish/completions/exhaustive.fish
+++ b/clap_complete/tests/snapshots/home/static/exhaustive/fish/fish/completions/exhaustive.fish
@@ -136,7 +136,7 @@ complete -c exhaustive -n "__fish_exhaustive_using_subcommand hint" -l email -r
 complete -c exhaustive -n "__fish_exhaustive_using_subcommand hint" -l global -d 'everywhere'
 complete -c exhaustive -n "__fish_exhaustive_using_subcommand hint" -s h -l help -d 'Print help'
 complete -c exhaustive -n "__fish_exhaustive_using_subcommand hint" -s V -l version -d 'Print version'
-complete -c exhaustive -n "__fish_exhaustive_using_subcommand complete" -l shell -d 'Specify shell to complete for' -r -f -a "{bash\t'',fish\t'',zsh\t'',elvish\t'',powershell\t''}"
+complete -c exhaustive -n "__fish_exhaustive_using_subcommand complete" -l shell -d 'Specify shell to complete for' -r -f -a "{bash\t'',elvish\t'',fish\t'',powershell\t'',zsh\t''}"
 complete -c exhaustive -n "__fish_exhaustive_using_subcommand complete" -l register -d 'Path to write completion-registration to' -r -F
 complete -c exhaustive -n "__fish_exhaustive_using_subcommand complete" -l global -d 'everywhere'
 complete -c exhaustive -n "__fish_exhaustive_using_subcommand complete" -s h -l help -d 'Print help (see more with \'--help\')'
diff --git a/clap_complete/tests/snapshots/home/static/exhaustive/zsh/zsh/_exhaustive b/clap_complete/tests/snapshots/home/static/exhaustive/zsh/zsh/_exhaustive
index 9683cac4..d596f71e 100644
--- a/clap_complete/tests/snapshots/home/static/exhaustive/zsh/zsh/_exhaustive
+++ b/clap_complete/tests/snapshots/home/static/exhaustive/zsh/zsh/_exhaustive
@@ -325,7 +325,7 @@ _arguments "${_arguments_options[@]}" : \
 ;;
 (complete)
 _arguments "${_arguments_options[@]}" : \
-'--shell=[Specify shell to complete for]:SHELL:(bash fish zsh elvish powershell)' \
+'--shell=[Specify shell to complete for]:SHELL:(bash elvish fish powershell zsh)' \
 '--register=[Path to write completion-registration to]:REGISTER:_files' \
 '--global[everywhere]' \
 '-h[Print help (see more with '\''--help'\'')]' \