Repository: clap-rs/clap
Pull request #5669 (2024-08-12 13:29:24)

fix(complete): Correct version check
<!--
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/candidate.rs b/clap_complete/src/dynamic/candidate.rs
index 517e2879..8123964d 100644
--- a/clap_complete/src/dynamic/candidate.rs
+++ b/clap_complete/src/dynamic/candidate.rs
@@ -4,9 +4,6 @@ use std::ffi::OsString;
 use clap::builder::StyledStr;
 
 /// A shell-agnostic completion candidate
-///
-/// [`Shell`][crate::dynamic::shells::Shell] will adapt what it can to the
-/// current shell.
 #[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord)]
 pub struct CompletionCandidate {
     content: OsString,
diff --git a/clap_complete/src/dynamic/command/mod.rs b/clap_complete/src/dynamic/command/mod.rs
new file mode 100644
index 00000000..799d56b6
--- /dev/null
+++ b/clap_complete/src/dynamic/command/mod.rs
@@ -0,0 +1,230 @@
+//! [`<bin> complete`][CompleteCommand] completion integration
+//!
+//! - If you aren't using a subcommand, see [`CompleteCommand`]
+//! - If you are using subcommands, see [`CompleteArgs`]
+//!
+//! To source your completions:
+//!
+//! Bash
+//! ```bash
+//! echo "source <(your_program complete bash)" >> ~/.bashrc
+//! ```
+//!
+//! Elvish
+//! ```elvish
+//! echo "eval (your_program complete elvish)" >> ~/.elvish/rc.elv
+//! ```
+//!
+//! Fish
+//! ```fish
+//! echo "source (your_program complete fish | psub)" >> ~/.config/fish/config.fish
+//! ```
+//!
+//! Powershell
+//! ```powershell
+//! echo "your_program complete powershell | Invoke-Expression" >> $PROFILE
+//! ```
+//!
+//! Zsh
+//! ```zsh
+//! echo "source <(your_program complete zsh)" >> ~/.zshrc
+//! ```
+
+mod shells;
+
+use std::ffi::OsString;
+use std::io::Write as _;
+
+pub use shells::*;
+
+/// A completion subcommand to add to your CLI
+///
+/// **Warning:** `stdout` should not be written to before [`CompleteCommand::complete`] has had a
+/// chance to run.
+///
+/// # Examples
+///
+/// To integrate completions into an application without subcommands:
+/// ```no_run
+/// // src/main.rs
+/// use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
+/// use clap_complete::dynamic::CompleteCommand;
+///
+/// #[derive(Parser, Debug)]
+/// #[clap(name = "dynamic", about = "A dynamic command line tool")]
+/// struct Cli {
+///     /// The subcommand to run complete
+///     #[command(subcommand)]
+///     complete: Option<CompleteCommand>,
+///
+///     /// Input file path
+///     #[clap(short, long, value_hint = clap::ValueHint::FilePath)]
+///     input: Option<String>,
+///     /// Output format
+///     #[clap(short = 'F', long, value_parser = ["json", "yaml", "toml"])]
+///     format: Option<String>,
+/// }
+///
+/// fn main() {
+///     let cli = Cli::parse();
+///     if let Some(completions) = cli.complete {
+///         completions.complete(&mut Cli::command());
+///     }
+///
+///     // normal logic continues...
+/// }
+///```
+#[derive(clap::Subcommand)]
+#[allow(missing_docs)]
+#[derive(Clone, Debug)]
+#[command(about = None, long_about = None)]
+pub enum CompleteCommand {
+    /// Register shell completions for this program
+    #[command(hide = true)]
+    Complete(CompleteArgs),
+}
+
+impl CompleteCommand {
+    /// Process the completion request and exit
+    ///
+    /// **Warning:** `stdout` should not be written to before this has had a
+    /// chance to run.
+    pub fn complete(&self, cmd: &mut clap::Command) -> std::convert::Infallible {
+        self.try_complete(cmd).unwrap_or_else(|e| e.exit());
+        std::process::exit(0)
+    }
+
+    /// Process the completion request
+    ///
+    /// **Warning:** `stdout` should not be written to before or after this has run.
+    pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> {
+        debug!("CompleteCommand::try_complete: {self:?}");
+        let CompleteCommand::Complete(args) = self;
+        args.try_complete(cmd)
+    }
+}
+
+/// A completion subcommand to add to your CLI
+///
+/// **Warning:** `stdout` should not be written to before [`CompleteArgs::complete`] has had a
+/// chance to run.
+///
+/// # Examples
+///
+/// To integrate completions into an application without subcommands:
+/// ```no_run
+/// // src/main.rs
+/// use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
+/// use clap_complete::dynamic::CompleteArgs;
+///
+/// #[derive(Parser, Debug)]
+/// #[clap(name = "dynamic", about = "A dynamic command line tool")]
+/// struct Cli {
+///     #[command(subcommand)]
+///     complete: Command,
+/// }
+///
+/// #[derive(Subcommand, Debug)]
+/// enum Command {
+///     Complete(CompleteArgs),
+///     Print,
+/// }
+///
+/// fn main() {
+///     let cli = Cli::parse();
+///     match cli.complete {
+///         Command::Complete(completions) => {
+///             completions.complete(&mut Cli::command());
+///         },
+///         Command::Print => {
+///             println!("Hello world!");
+///         }
+///     }
+/// }
+///```
+#[derive(clap::Args, Clone, Debug)]
+#[command(about = None, long_about = None)]
+pub struct CompleteArgs {
+    /// Specify shell to complete for
+    #[arg(value_name = "NAME")]
+    shell: Option<Shell>,
+
+    #[arg(raw = true, value_name = "ARG", hide = true)]
+    comp_words: Option<Vec<OsString>>,
+}
+
+impl CompleteArgs {
+    /// Process the completion request and exit
+    ///
+    /// **Warning:** `stdout` should not be written to before this has had a
+    /// chance to run.
+    pub fn complete(&self, cmd: &mut clap::Command) -> std::convert::Infallible {
+        self.try_complete(cmd).unwrap_or_else(|e| e.exit());
+        std::process::exit(0)
+    }
+
+    /// Process the completion request
+    ///
+    /// **Warning:** `stdout` should not be written to before or after this has run.
+    pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> {
+        debug!("CompleteCommand::try_complete: {self:?}");
+
+        let shell = self.shell.or_else(Shell::from_env).ok_or_else(|| {
+            std::io::Error::new(
+                std::io::ErrorKind::Other,
+                "unknown shell, please specify the name of your shell",
+            )
+        })?;
+
+        if let Some(comp_words) = self.comp_words.as_ref() {
+            let current_dir = std::env::current_dir().ok();
+
+            let mut buf = Vec::new();
+            shell.write_complete(cmd, comp_words.clone(), current_dir.as_deref(), &mut buf)?;
+            std::io::stdout().write_all(&buf)?;
+        } else {
+            let name = cmd.get_name();
+            let bin = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name());
+
+            let mut buf = Vec::new();
+            shell.write_registration(name, bin, bin, &mut buf)?;
+            std::io::stdout().write_all(&buf)?;
+        }
+
+        Ok(())
+    }
+}
+
+/// Shell-integration for completions
+///
+/// This will generally be called by [`CompleteCommand`] or [`CompleteArgs`].
+///
+/// This handles adapting between the shell and [`completer`][crate::dynamic::complete()].
+/// A `CommandCompleter` can choose how much of that lives within the registration script and or
+/// lives in [`CommandCompleter::write_complete`].
+pub trait CommandCompleter {
+    /// Register for completions
+    ///
+    /// Write the `buf` the logic needed for calling into `<cmd> complete`, passing needed
+    /// arguments to [`CommandCompleter::write_complete`] through the environment.
+    fn write_registration(
+        &self,
+        name: &str,
+        bin: &str,
+        completer: &str,
+        buf: &mut dyn std::io::Write,
+    ) -> Result<(), std::io::Error>;
+    /// Complete the given command
+    ///
+    /// Adapt information from arguments and [`CommandCompleter::write_registration`]-defined env
+    /// variables to what is needed for [`completer`][crate::dynamic::complete()].
+    ///
+    /// Write out the [`CompletionCandidate`][crate::dynamic::CompletionCandidate]s in a way the shell will understand.
+    fn write_complete(
+        &self,
+        cmd: &mut clap::Command,
+        args: Vec<OsString>,
+        current_dir: Option<&std::path::Path>,
+        buf: &mut dyn std::io::Write,
+    ) -> Result<(), std::io::Error>;
+}
diff --git a/clap_complete/src/dynamic/shells/command.rs b/clap_complete/src/dynamic/command/shells.rs
similarity index 55%
rename from clap_complete/src/dynamic/shells/command.rs
rename to clap_complete/src/dynamic/command/shells.rs
index 05664586..d7b8b70f 100644
--- a/clap_complete/src/dynamic/shells/command.rs
+++ b/clap_complete/src/dynamic/command/shells.rs
@@ -1,264 +1,84 @@
 use std::ffi::OsString;
-use std::io::Write as _;
+use std::fmt::Display;
+use std::str::FromStr;
 
+use clap::builder::PossibleValue;
+use clap::ValueEnum;
 use unicode_xid::UnicodeXID as _;
 
-use super::Shell;
-
-/// A completion subcommand to add to your CLI
-///
-/// If you aren't using a subcommand, you can annotate a field with this type as `#[command(subcommand)]`.
-///
-/// If you are using subcommands, see [`CompleteArgs`].
-///
-/// **Warning:** `stdout` should not be written to before [`CompleteCommand::complete`] has had a
-/// chance to run.
-///
-/// # Examples
-///
-/// To integrate completions into an application without subcommands:
-/// ```no_run
-/// // src/main.rs
-/// use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
-/// use clap_complete::dynamic::CompleteCommand;
-///
-/// #[derive(Parser, Debug)]
-/// #[clap(name = "dynamic", about = "A dynamic command line tool")]
-/// struct Cli {
-///     /// The subcommand to run complete
-///     #[command(subcommand)]
-///     complete: Option<CompleteCommand>,
-///     /// Input file path
-///     #[clap(short, long, value_hint = clap::ValueHint::FilePath)]
-///     input: Option<String>,
-///     /// Output format
-///     #[clap(short = 'F', long, value_parser = ["json", "yaml", "toml"])]
-///     format: Option<String>,
-/// }
-///
-/// fn main() {
-///     let cli = Cli::parse();
-///     if let Some(completions) = cli.complete {
-///         completions.complete(&mut Cli::command());
-///     }
-///
-///     // normal logic continues...
-/// }
-///```
-///
-/// To source your completions:
-///
-/// Bash
-/// ```bash
-/// echo "source <(your_program complete bash --register -)" >> ~/.bashrc
-/// ```
-///
-/// Elvish
-/// ```elvish
-/// echo "eval (your_program complete elvish --register -)" >> ~/.elvish/rc.elv
-/// ```
-///
-/// Fish
-/// ```fish
-/// echo "source (your_program complete fish --register - | psub)" >> ~/.config/fish/config.fish
-/// ```
-///
-/// Powershell
-/// ```powershell
-/// echo "your_program complete powershell --register - | Invoke-Expression" >> $PROFILE
-/// ```
-///
-/// Zsh
-/// ```zsh
-/// echo "source <(your_program complete zsh --register -)" >> ~/.zshrc
-/// ```
-#[derive(clap::Subcommand)]
-#[allow(missing_docs)]
-#[derive(Clone, Debug)]
-#[command(about = None, long_about = None)]
-#[cfg(feature = "unstable-command")]
-pub enum CompleteCommand {
-    /// Register shell completions for this program
-    #[command(hide = true)]
-    Complete(CompleteArgs),
+use super::CommandCompleter;
+
+/// Completion support for built-in shells
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+#[non_exhaustive]
+pub enum Shell {
+    /// Bourne Again `SHell` (bash)
+    Bash,
+    /// Elvish shell
+    Elvish,
+    /// Friendly Interactive `SHell` (fish)
+    Fish,
+    /// `PowerShell`
+    Powershell,
+    /// Z `SHell` (zsh)
+    Zsh,
 }
 
-impl CompleteCommand {
-    /// Process the completion request and exit
+impl Shell {
+    /// Parse a shell from a path to the executable for the shell
     ///
-    /// **Warning:** `stdout` should not be written to before this has had a
-    /// chance to run.
-    pub fn complete(&self, cmd: &mut clap::Command) -> std::convert::Infallible {
-        self.try_complete(cmd).unwrap_or_else(|e| e.exit());
-        std::process::exit(0)
-    }
-
-    /// Process the completion request
+    /// # Examples
     ///
-    /// **Warning:** `stdout` should not be written to before or after this has run.
-    pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> {
-        debug!("CompleteCommand::try_complete: {self:?}");
-        let CompleteCommand::Complete(args) = self;
-        args.try_complete(cmd)
-    }
-}
-
-/// A completion subcommand to add to your CLI
-///
-/// If you are using subcommands, add a `Complete(CompleteArgs)` variant.
-///
-/// If you aren't using subcommands, generally you will want [`CompleteCommand`].
-///
-/// **Warning:** `stdout` should not be written to before [`CompleteArgs::complete`] has had a
-/// chance to run.
-///
-/// # Examples
-///
-/// To integrate completions into an application without subcommands:
-/// ```no_run
-/// // src/main.rs
-/// use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
-/// use clap_complete::dynamic::CompleteArgs;
-///
-/// #[derive(Parser, Debug)]
-/// #[clap(name = "dynamic", about = "A dynamic command line tool")]
-/// struct Cli {
-///     #[command(subcommand)]
-///     complete: Command,
-/// }
-///
-/// #[derive(Subcommand, Debug)]
-/// enum Command {
-///     Complete(CompleteArgs),
-///     Print,
-/// }
-///
-/// fn main() {
-///     let cli = Cli::parse();
-///     match cli.complete {
-///         Command::Complete(completions) => {
-///             completions.complete(&mut Cli::command());
-///         },
-///         Command::Print => {
-///             println!("Hello world!");
-///         }
-///     }
-/// }
-///```
-///
-/// To source your completions:
-///
-/// Bash
-/// ```bash
-/// echo "source <(your_program complete bash)" >> ~/.bashrc
-/// ```
-///
-/// Elvish
-/// ```elvish
-/// echo "eval (your_program complete elvish)" >> ~/.elvish/rc.elv
-/// ```
-///
-/// Fish
-/// ```fish
-/// echo "source (your_program complete fish | psub)" >> ~/.config/fish/config.fish
-/// ```
-///
-/// Powershell
-/// ```powershell
-/// echo "your_program complete powershell | Invoke-Expression" >> $PROFILE
-/// ```
-///
-/// Zsh
-/// ```zsh
-/// echo "source <(your_program complete zsh)" >> ~/.zshrc
-/// ```
-#[derive(clap::Args, Clone, Debug)]
-#[command(about = None, long_about = None)]
-#[cfg(feature = "unstable-command")]
-pub struct CompleteArgs {
-    /// Specify shell to complete for
-    #[arg(value_name = "NAME")]
-    shell: Option<Shell>,
-
-    #[arg(raw = true, value_name = "ARG", hide = true)]
-    comp_words: Option<Vec<OsString>>,
-}
-
-impl CompleteArgs {
-    /// Process the completion request and exit
+    /// ```
+    /// use clap_complete::shells::Shell;
     ///
-    /// **Warning:** `stdout` should not be written to before this has had a
-    /// chance to run.
-    pub fn complete(&self, cmd: &mut clap::Command) -> std::convert::Infallible {
-        self.try_complete(cmd).unwrap_or_else(|e| e.exit());
-        std::process::exit(0)
+    /// 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())
     }
 
-    /// Process the completion request
+    /// 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`.
     ///
-    /// **Warning:** `stdout` should not be written to before or after this has run.
-    pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> {
-        debug!("CompleteCommand::try_complete: {self:?}");
-
-        let shell = self.shell.or_else(|| Shell::from_env()).ok_or_else(|| {
-            std::io::Error::new(
-                std::io::ErrorKind::Other,
-                "unknown shell, please specify the name of your shell",
-            )
-        })?;
-
-        if let Some(comp_words) = self.comp_words.as_ref() {
-            let current_dir = std::env::current_dir().ok();
-
-            let mut buf = Vec::new();
-            shell.write_complete(cmd, comp_words.clone(), current_dir.as_deref(), &mut buf)?;
-            std::io::stdout().write_all(&buf)?;
+    /// # 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 {
-            let name = cmd.get_name();
-            let bin = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name());
-
-            let mut buf = Vec::new();
-            shell.write_registration(name, bin, bin, &mut buf)?;
-            std::io::stdout().write_all(&buf)?;
+            None
         }
-
-        Ok(())
     }
-}
 
-/// Shell-integration for completions
-///
-/// This will generally be called by [`CompleteCommand`] or [`CompleteArgs`].
-///
-/// This handles adapting between the shell and [`completer`][crate::dynamic::complete()].
-/// A `CommandCompleter` can choose how much of that lives within the registration script and or
-/// lives in [`CommandCompleter::write_complete`].
-#[cfg(feature = "unstable-command")]
-pub trait CommandCompleter {
-    /// Register for completions
-    ///
-    /// Write the `buf` the logic needed for calling into `<cmd> complete`, passing needed
-    /// arguments to [`CommandCompleter::write_complete`] through the environment.
-    fn write_registration(
-        &self,
-        name: &str,
-        bin: &str,
-        completer: &str,
-        buf: &mut dyn std::io::Write,
-    ) -> Result<(), std::io::Error>;
-    /// Complete the given command
-    ///
-    /// Adapt information from arguments and [`CommandCompleter::write_registration`]-defined env
-    /// variables to what is needed for [`completer`][crate::dynamic::complete()].
-    ///
-    /// Write out the [`CompletionCandidate`][crate::dynamic::CompletionCandidate]s in a way the shell will understand.
-    fn write_complete(
-        &self,
-        cmd: &mut clap::Command,
-        args: Vec<OsString>,
-        current_dir: Option<&std::path::Path>,
-        buf: &mut dyn std::io::Write,
-    ) -> Result<(), std::io::Error>;
+    fn completer(&self) -> &dyn CommandCompleter {
+        match self {
+            Self::Bash => &Bash,
+            Self::Elvish => &Elvish,
+            Self::Fish => &Fish,
+            Self::Powershell => &Powershell,
+            Self::Zsh => &Zsh,
+        }
+    }
 }
 
 impl CommandCompleter for Shell {
@@ -269,7 +89,8 @@ impl CommandCompleter for Shell {
         completer: &str,
         buf: &mut dyn std::io::Write,
     ) -> Result<(), std::io::Error> {
-        shell_completer(self).write_registration(name, bin, completer, buf)
+        self.completer()
+            .write_registration(name, bin, completer, buf)
     }
     fn write_complete(
         &self,
@@ -278,21 +99,74 @@ impl CommandCompleter for Shell {
         current_dir: Option<&std::path::Path>,
         buf: &mut dyn std::io::Write,
     ) -> Result<(), std::io::Error> {
-        shell_completer(self).write_complete(cmd, args, current_dir, buf)
+        self.completer().write_complete(cmd, args, current_dir, buf)
+    }
+}
+
+// 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 {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.to_possible_value()
+            .expect("no values are skipped")
+            .get_name()
+            .fmt(f)
+    }
+}
+
+impl FromStr for Shell {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        for variant in Self::value_variants() {
+            if variant.to_possible_value().unwrap().matches(s, false) {
+                return Ok(*variant);
+            }
+        }
+        Err(format!("invalid variant: {s}"))
     }
 }
 
-fn shell_completer(shell: &Shell) -> &dyn CommandCompleter {
-    match shell {
-        Shell::Bash => &super::Bash,
-        Shell::Elvish => &super::Elvish,
-        Shell::Fish => &super::Fish,
-        Shell::Powershell => &super::Powershell,
-        Shell::Zsh => &super::Zsh,
+// Hand-rolled so it can work even when `derive` feature is disabled
+impl ValueEnum for Shell {
+    fn value_variants<'a>() -> &'a [Self] {
+        &[
+            Shell::Bash,
+            Shell::Elvish,
+            Shell::Fish,
+            Shell::Powershell,
+            Shell::Zsh,
+        ]
+    }
+
+    fn to_possible_value(&self) -> Option<PossibleValue> {
+        Some(match self {
+            Shell::Bash => PossibleValue::new("bash"),
+            Shell::Elvish => PossibleValue::new("elvish"),
+            Shell::Fish => PossibleValue::new("fish"),
+            Shell::Powershell => PossibleValue::new("powershell"),
+            Shell::Zsh => PossibleValue::new("zsh"),
+        })
     }
 }
 
-impl CommandCompleter for super::Bash {
+/// Bash completion adapter
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub struct Bash;
+
+impl CommandCompleter for Bash {
     fn write_registration(
         &self,
         name: &str,
@@ -328,7 +202,7 @@ _clap_complete_NAME() {
         compopt -o nospace
     fi
 }
-if [[ \"${{BASH_VERSINFO[0]}}\" -eq 4 && \"${{BASH_VERSINFO[1]}}\" -ge 4 || \"${{BASH_VERSINFO[0]}}\" -gt 4 ]]; then
+if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
     complete -o nospace -o bashdefault -o nosort -F _clap_complete_NAME BIN
 else
     complete -o nospace -o bashdefault -F _clap_complete_NAME BIN
@@ -389,7 +263,7 @@ enum CompType {
     Menu,
 }
 
-impl std::str::FromStr for CompType {
+impl FromStr for CompType {
     type Err = String;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
@@ -409,7 +283,12 @@ impl Default for CompType {
         Self::Normal
     }
 }
-impl CommandCompleter for super::Elvish {
+
+/// Elvish completion adapter
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub struct Elvish;
+
+impl CommandCompleter for Elvish {
     fn write_registration(
         &self,
         _name: &str,
@@ -462,7 +341,11 @@ set edit:completion:arg-completer[BIN] = { |@words|
     }
 }
 
-impl CommandCompleter for super::Fish {
+/// Fish completion adapter
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub struct Fish;
+
+impl CommandCompleter for Fish {
     fn write_registration(
         &self,
         _name: &str,
@@ -504,7 +387,11 @@ impl CommandCompleter for super::Fish {
     }
 }
 
-impl CommandCompleter for super::Powershell {
+/// Powershell completion adapter
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub struct Powershell;
+
+impl CommandCompleter for Powershell {
     fn write_registration(
         &self,
         _name: &str,
@@ -533,7 +420,7 @@ Register-ArgumentCompleter -Native -CommandName {bin} -ScriptBlock {{
         else {{
             $help = $split[0];
         }}
-        
+
         [System.Management.Automation.CompletionResult]::new($cmd, $cmd, 'ParameterValue', $help)
     }}
 }};
@@ -566,7 +453,11 @@ Register-ArgumentCompleter -Native -CommandName {bin} -ScriptBlock {{
     }
 }
 
-impl CommandCompleter for super::Zsh {
+/// Zsh completion adapter
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub struct Zsh;
+
+impl CommandCompleter for Zsh {
     fn write_registration(
         &self,
         _name: &str,
diff --git a/clap_complete/src/dynamic/complete.rs b/clap_complete/src/dynamic/complete.rs
index 214e3d65..256eb26b 100644
--- a/clap_complete/src/dynamic/complete.rs
+++ b/clap_complete/src/dynamic/complete.rs
@@ -7,11 +7,6 @@ use super::ArgValueCompleter;
 use super::CompletionCandidate;
 
 /// Complete the given command, shell-agnostic
-///
-/// For integration with clap and shells, see [`CompleteCommand`][crate::dynamic::shells::CompleteCommand].
-///
-/// This is generally called by a [`Shell`][crate::dynamic::shells::Shell] which
-/// handles the shell-specific logic.
 pub fn complete(
     cmd: &mut clap::Command,
     args: Vec<OsString>,
diff --git a/clap_complete/src/dynamic/mod.rs b/clap_complete/src/dynamic/mod.rs
index 3ccb879e..6adc7931 100644
--- a/clap_complete/src/dynamic/mod.rs
+++ b/clap_complete/src/dynamic/mod.rs
@@ -1,7 +1,5 @@
 //! Complete commands within shells
 //!
-//! For quick-start, see [`CompleteCommand`]
-//!
 //! To customize completions, see
 //! - [`ValueHint`][crate::ValueHint]
 //! - [`ValueEnum`][clap::ValueEnum]
@@ -11,15 +9,15 @@ mod candidate;
 mod complete;
 mod custom;
 
-pub mod shells;
+#[cfg(feature = "unstable-command")]
+pub mod command;
 
 pub use candidate::CompletionCandidate;
 pub use complete::complete;
 pub use custom::ArgValueCompleter;
 pub use custom::CustomCompleter;
 
-// These live in `shells` because they are tightly coupled with the `ShellCompleter`s
 #[cfg(feature = "unstable-command")]
-pub use shells::CompleteArgs;
+pub use command::CompleteArgs;
 #[cfg(feature = "unstable-command")]
-pub use shells::CompleteCommand;
+pub use command::CompleteCommand;
diff --git a/clap_complete/src/dynamic/shells/mod.rs b/clap_complete/src/dynamic/shells/mod.rs
deleted file mode 100644
index 147d95ae..00000000
--- a/clap_complete/src/dynamic/shells/mod.rs
+++ /dev/null
@@ -1,155 +0,0 @@
-//! Shell completion support, see [`CompleteCommand`] for more details
-
-#[cfg(feature = "unstable-command")]
-mod command;
-
-#[cfg(feature = "unstable-command")]
-pub use command::*;
-
-use std::fmt::Display;
-use std::str::FromStr;
-
-use clap::builder::PossibleValue;
-use clap::ValueEnum;
-
-/// Completion support for built-in shells
-#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
-#[non_exhaustive]
-pub enum Shell {
-    /// Bourne Again `SHell` (bash)
-    Bash,
-    /// Elvish shell
-    Elvish,
-    /// Friendly Interactive `SHell` (fish)
-    Fish,
-    /// `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 {
-            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 {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.to_possible_value()
-            .expect("no values are skipped")
-            .get_name()
-            .fmt(f)
-    }
-}
-
-impl FromStr for Shell {
-    type Err = String;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        for variant in Self::value_variants() {
-            if variant.to_possible_value().unwrap().matches(s, false) {
-                return Ok(*variant);
-            }
-        }
-        Err(format!("invalid variant: {s}"))
-    }
-}
-
-// Hand-rolled so it can work even when `derive` feature is disabled
-impl ValueEnum for Shell {
-    fn value_variants<'a>() -> &'a [Self] {
-        &[
-            Shell::Bash,
-            Shell::Elvish,
-            Shell::Fish,
-            Shell::Powershell,
-            Shell::Zsh,
-        ]
-    }
-
-    fn to_possible_value(&self) -> Option<PossibleValue> {
-        Some(match self {
-            Shell::Bash => PossibleValue::new("bash"),
-            Shell::Elvish => PossibleValue::new("elvish"),
-            Shell::Fish => PossibleValue::new("fish"),
-            Shell::Powershell => PossibleValue::new("powershell"),
-            Shell::Zsh => PossibleValue::new("zsh"),
-        })
-    }
-}
-
-/// Bash completion adapter
-#[derive(Copy, Clone, PartialEq, Eq, Debug)]
-pub struct Bash;
-
-/// Elvish completion adapter
-#[derive(Copy, Clone, PartialEq, Eq, Debug)]
-pub struct Elvish;
-
-/// Fish completion adapter
-#[derive(Copy, Clone, PartialEq, Eq, Debug)]
-pub struct Fish;
-
-/// Powershell completion adapter
-#[derive(Copy, Clone, PartialEq, Eq, Debug)]
-pub struct Powershell;
-
-/// Zsh completion adapter
-#[derive(Copy, Clone, PartialEq, Eq, Debug)]
-pub struct Zsh;


Test patch:
diff --git a/clap_complete/tests/snapshots/home/dynamic/exhaustive/bash/.bashrc b/clap_complete/tests/snapshots/home/dynamic/exhaustive/bash/.bashrc
index b10dc26e..668029e8 100644
--- a/clap_complete/tests/snapshots/home/dynamic/exhaustive/bash/.bashrc
+++ b/clap_complete/tests/snapshots/home/dynamic/exhaustive/bash/.bashrc
@@ -17,7 +17,7 @@ _clap_complete_exhaustive() {
         compopt -o nospace
     fi
 }
-if [[ \"${{BASH_VERSINFO[0]}}\" -eq 4 && \"${{BASH_VERSINFO[1]}}\" -ge 4 || \"${{BASH_VERSINFO[0]}}\" -gt 4 ]]; then
+if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
     complete -o nospace -o bashdefault -o nosort -F _clap_complete_exhaustive exhaustive
 else
     complete -o nospace -o bashdefault -F _clap_complete_exhaustive exhaustive
diff --git a/clap_complete/tests/snapshots/register_minimal.bash b/clap_complete/tests/snapshots/register_minimal.bash
index a111d3d9..925c3835 100644
--- a/clap_complete/tests/snapshots/register_minimal.bash
+++ b/clap_complete/tests/snapshots/register_minimal.bash
@@ -15,7 +15,7 @@ _clap_complete_my_app() {
         compopt -o nospace
     fi
 }
-if [[ /"${{BASH_VERSINFO[0]}}/" -eq 4 && /"${{BASH_VERSINFO[1]}}/" -ge 4 || /"${{BASH_VERSINFO[0]}}/" -gt 4 ]]; then
+if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
     complete -o nospace -o bashdefault -o nosort -F _clap_complete_my_app my-app
 else
     complete -o nospace -o bashdefault -F _clap_complete_my_app my-app
diff --git a/clap_complete/tests/testsuite/bash.rs b/clap_complete/tests/testsuite/bash.rs
index c5d9267f..d39debbc 100644
--- a/clap_complete/tests/testsuite/bash.rs
+++ b/clap_complete/tests/testsuite/bash.rs
@@ -114,14 +114,14 @@ fn value_terminator() {
 #[cfg(feature = "unstable-command")]
 #[test]
 fn register_minimal() {
-    use clap_complete::dynamic::shells::CommandCompleter as _;
+    use clap_complete::dynamic::command::CommandCompleter as _;
 
     let name = "my-app";
     let bin = name;
     let completer = name;
 
     let mut buf = Vec::new();
-    clap_complete::dynamic::shells::Bash
+    clap_complete::dynamic::command::Bash
         .write_registration(name, bin, completer, &mut buf)
         .unwrap();
     snapbox::Assert::new()
@@ -249,7 +249,39 @@ fn complete() {
 }
 
 #[test]
-#[cfg(unix)]
+#[cfg(all(unix, feature = "unstable-command"))]
 fn register_dynamic_completion() {
     common::register_example::<completest_pty::BashRuntimeBuilder>("dynamic", "exhaustive");
 }
+
+#[test]
+#[cfg(all(unix, feature = "unstable-command"))]
+fn complete_dynamic() {
+    if !common::has_command("bash") {
+        return;
+    }
+
+    let term = completest::Term::new();
+    let mut runtime =
+        common::load_runtime::<completest_pty::BashRuntimeBuilder>("dynamic", "exhaustive");
+
+    let input = "exhaustive \t\t";
+    let expected = snapbox::str![[r#"
+% 
+--global    --help      -h          action      help        last        quote       
+--generate  --version   -V          alias       hint        pacman      value       
+"#]];
+    let actual = runtime.complete(input, &term).unwrap();
+    assert_data_eq!(actual, expected);
+
+    let input = "exhaustive quote \t\t";
+    let expected = snapbox::str![[r#"
+% 
+--single-quotes    --brackets         --help             cmd-backslash      cmd-expansions     
+--double-quotes    --expansions       --version          cmd-backticks      cmd-single-quotes  
+--backticks        --choice           -h                 cmd-brackets       escape-help        
+--backslash        --global           -V                 cmd-double-quotes  help               
+"#]];
+    let actual = runtime.complete(input, &term).unwrap();
+    assert_data_eq!(actual, expected);
+}
diff --git a/clap_complete/tests/testsuite/elvish.rs b/clap_complete/tests/testsuite/elvish.rs
index 9feeb0b4..b953fd8b 100644
--- a/clap_complete/tests/testsuite/elvish.rs
+++ b/clap_complete/tests/testsuite/elvish.rs
@@ -175,8 +175,8 @@ value          value
     assert_data_eq!(actual, expected);
 }
 
-#[cfg(all(unix, feature = "unstable-command"))]
 #[test]
+#[cfg(all(unix, feature = "unstable-command"))]
 fn register_dynamic() {
     common::register_example::<completest_pty::ElvishRuntimeBuilder>("dynamic", "exhaustive");
 }
diff --git a/clap_complete/tests/testsuite/fish.rs b/clap_complete/tests/testsuite/fish.rs
index f27db021..a4eaddab 100644
--- a/clap_complete/tests/testsuite/fish.rs
+++ b/clap_complete/tests/testsuite/fish.rs
@@ -165,8 +165,8 @@ bash  (bash (shell))  fish  (fish shell)  zsh  (zsh shell)"#;
     assert_data_eq!(actual, expected);
 }
 
-#[cfg(all(unix, feature = "unstable-command"))]
 #[test]
+#[cfg(all(unix, feature = "unstable-command"))]
 fn register_dynamic() {
     common::register_example::<completest_pty::FishRuntimeBuilder>("dynamic", "exhaustive");
 }
diff --git a/clap_complete/tests/testsuite/zsh.rs b/clap_complete/tests/testsuite/zsh.rs
index b5abb971..d0b2f503 100644
--- a/clap_complete/tests/testsuite/zsh.rs
+++ b/clap_complete/tests/testsuite/zsh.rs
@@ -163,8 +163,8 @@ pacman    action  alias  value  quote  hint  last  --
     assert_data_eq!(actual, expected);
 }
 
-#[cfg(all(unix, feature = "unstable-command"))]
 #[test]
+#[cfg(all(unix, feature = "unstable-command"))]
 fn register_dynamic() {
     common::register_example::<completest_pty::ZshRuntimeBuilder>("dynamic", "exhaustive");
 }