Skip to content

Commit

Permalink
Show appropriate activation command based on shell detection
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Mar 5, 2024
1 parent 9e41f73 commit faed080
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 21 deletions.
67 changes: 46 additions & 21 deletions crates/uv/src/commands/venv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<Path>) -> 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
}
}
1 change: 1 addition & 0 deletions crates/uv/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ mod confirm;
mod logging;
mod printer;
mod requirements;
mod shell;
mod version;

const DEFAULT_VENV_NAME: &str = ".venv";
Expand Down
69 changes: 69 additions & 0 deletions crates/uv/src/shell.rs
Original file line number Diff line number Diff line change
@@ -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<Shell> {
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<P: AsRef<Path>>(path: P) -> Option<Shell> {
parse_shell_from_path(path.as_ref())
}
}

fn parse_shell_from_path(path: &Path) -> Option<Shell> {
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,
}
}

0 comments on commit faed080

Please sign in to comment.