diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 9dcd2fec344f..e37b7568b1d8 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -25,6 +25,7 @@ use uv_traits::{BuildContext, ConfigSettings, InFlight, NoBuild, SetupPyStrategy use crate::commands::ExitStatus; use crate::printer::Printer; +use crate::shell::Shell; /// Create a virtual environment. #[allow(clippy::unnecessary_wraps, clippy::too_many_arguments)] @@ -210,29 +211,53 @@ async fn venv_impl( } } - if cfg!(windows) { - writeln!( - printer, - // This should work whether the user is on CMD or PowerShell: - "Activate with: {}", - path.join("Scripts") - .join("activate") - .simplified_display() - .green() - ) - .into_diagnostic()?; - } else { - writeln!( - printer, - "Activate with: {}", - format!( - "source {}", - path.join("bin").join("activate").simplified_display() + // Determine the appropriate activation command. + let activation = match Shell::from_env() { + None => None, + Some(Shell::Bash | Shell::Zsh) => Some(format!( + "source {}", + shlex(path.join("bin").join("activate")) + )), + Some(Shell::Fish) => Some(format!( + "source {}", + shlex(path.join("bin").join("activate.fish")) + )), + Some(Shell::Nushell) => Some(format!( + "overlay use {}", + shlex(path.join("bin").join("activate.nu")) + )), + Some(Shell::Csh) => Some(format!( + "source {}", + shlex(path.join("bin").join("activate.csh")) + )), + Some(Shell::Powershell) => { + // No need to quote the path for PowerShell. + Some( + path.join("Scripts") + .join("activate") + .simplified_display() + .to_string(), ) - .green() - ) - .into_diagnostic()?; + } }; + if let Some(act) = activation { + writeln!(printer, "Activate with: {}", act.green()).into_diagnostic()?; + } Ok(ExitStatus::Success) } + +/// Quote a path, if necessary, for safe use in a shell command. +fn shlex(executable: impl AsRef) -> String { + // Convert to a display path. + let executable = executable.as_ref().simplified_display().to_string(); + + // Like Python's `shlex.quote`: + // > Use single quotes, and put single quotes into double quotes + // > The string $'b is then quoted as '$'"'"'b' + if executable.contains(' ') { + format!("'{}'", executable.replace('\'', r#"'"'"'"#)) + } else { + executable + } +} diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index a20438a5f43b..e5c2cc6ee6b1 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -50,6 +50,7 @@ mod confirm; mod logging; mod printer; mod requirements; +mod shell; mod version; const DEFAULT_VENV_NAME: &str = ".venv"; diff --git a/crates/uv/src/shell.rs b/crates/uv/src/shell.rs new file mode 100644 index 000000000000..cc1cdbdb5da5 --- /dev/null +++ b/crates/uv/src/shell.rs @@ -0,0 +1,69 @@ +use std::path::Path; + +/// Shells for which virtualenv activation scripts are available. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub(crate) enum Shell { + /// Bourne Again SHell (bash) + Bash, + /// Friendly Interactive SHell (fish) + Fish, + /// PowerShell + Powershell, + /// Z SHell (zsh) + Zsh, + /// Nushell + Nushell, + /// C SHell (csh) + Csh, +} + +impl Shell { + /// 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 `OSes` 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`. + pub(crate) fn from_env() -> Option { + if std::env::var_os("NU_VERSION").is_some() { + Some(Shell::Nushell) + } else 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 + } + } + + /// Parse a shell from a path to the executable for the shell. + /// + /// # Examples + /// + /// ``` + /// use crate::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(crate) fn from_shell_path>(path: P) -> Option { + parse_shell_from_path(path.as_ref()) + } +} + +fn parse_shell_from_path(path: &Path) -> Option { + let name = path.file_stem()?.to_str()?; + match name { + "bash" => Some(Shell::Bash), + "zsh" => Some(Shell::Zsh), + "fish" => Some(Shell::Fish), + "csh" => Some(Shell::Csh), + "powershell" | "powershell_ise" => Some(Shell::Powershell), + _ => None, + } +}