mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-14 20:39:37 +00:00

## Summary Make the use of `Self` consistent. Mostly done by running `cargo clippy --fix -- -A clippy::all -W clippy::use_self`. ## Test Plan <!-- How was it tested? --> No need.
290 lines
11 KiB
Rust
290 lines
11 KiB
Rust
pub mod runnable;
|
|
mod shlex;
|
|
pub mod windows;
|
|
|
|
pub use shlex::{escape_posix_for_single_quotes, shlex_posix, shlex_windows};
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use uv_fs::Simplified;
|
|
use uv_static::EnvVars;
|
|
|
|
/// Shells for which virtualenv activation scripts are available.
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
#[allow(clippy::doc_markdown)]
|
|
pub enum Shell {
|
|
/// Bourne Again SHell (bash)
|
|
Bash,
|
|
/// Friendly Interactive SHell (fish)
|
|
Fish,
|
|
/// PowerShell
|
|
Powershell,
|
|
/// Cmd (Command Prompt)
|
|
Cmd,
|
|
/// Z SHell (zsh)
|
|
Zsh,
|
|
/// Nushell
|
|
Nushell,
|
|
/// C SHell (csh)
|
|
Csh,
|
|
/// Korn SHell (ksh)
|
|
Ksh,
|
|
}
|
|
|
|
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 fn from_env() -> Option<Self> {
|
|
if std::env::var_os(EnvVars::NU_VERSION).is_some() {
|
|
Some(Self::Nushell)
|
|
} else if std::env::var_os(EnvVars::FISH_VERSION).is_some() {
|
|
Some(Self::Fish)
|
|
} else if std::env::var_os(EnvVars::BASH_VERSION).is_some() {
|
|
Some(Self::Bash)
|
|
} else if std::env::var_os(EnvVars::ZSH_VERSION).is_some() {
|
|
Some(Self::Zsh)
|
|
} else if std::env::var_os(EnvVars::KSH_VERSION).is_some() {
|
|
Some(Self::Ksh)
|
|
} else if let Some(env_shell) = std::env::var_os(EnvVars::SHELL) {
|
|
Self::from_shell_path(env_shell)
|
|
} else if cfg!(windows) {
|
|
// Command Prompt relies on PROMPT for its appearance whereas PowerShell does not.
|
|
// See: https://stackoverflow.com/a/66415037.
|
|
if std::env::var_os(EnvVars::PROMPT).is_some() {
|
|
Some(Self::Cmd)
|
|
} else {
|
|
// Fallback to PowerShell if the PROMPT environment variable is not set.
|
|
Some(Self::Powershell)
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Parse a shell from a path to the executable for the shell.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```ignore
|
|
/// 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 fn from_shell_path(path: impl AsRef<Path>) -> Option<Self> {
|
|
parse_shell_from_path(path.as_ref())
|
|
}
|
|
|
|
/// Returns `true` if the shell supports a `PATH` update command.
|
|
pub fn supports_update(self) -> bool {
|
|
match self {
|
|
Self::Powershell | Self::Cmd => true,
|
|
shell => !shell.configuration_files().is_empty(),
|
|
}
|
|
}
|
|
|
|
/// Return the configuration files that should be modified to append to a shell's `PATH`.
|
|
///
|
|
/// Some of the logic here is based on rustup's rc file detection.
|
|
///
|
|
/// See: <https://github.com/rust-lang/rustup/blob/fede22fea7b160868cece632bd213e6d72f8912f/src/cli/self_update/shell.rs#L197>
|
|
pub fn configuration_files(self) -> Vec<PathBuf> {
|
|
let Some(home_dir) = home::home_dir() else {
|
|
return vec![];
|
|
};
|
|
match self {
|
|
Self::Bash => {
|
|
// On Bash, we need to update both `.bashrc` and `.bash_profile`. The former is
|
|
// sourced for non-login shells, and the latter is sourced for login shells.
|
|
//
|
|
// In lieu of `.bash_profile`, shells will also respect `.bash_login` and
|
|
// `.profile`, if they exist. So we respect those too.
|
|
vec![
|
|
[".bash_profile", ".bash_login", ".profile"]
|
|
.iter()
|
|
.map(|rc| home_dir.join(rc))
|
|
.find(|rc| rc.is_file())
|
|
.unwrap_or_else(|| home_dir.join(".bash_profile")),
|
|
home_dir.join(".bashrc"),
|
|
]
|
|
}
|
|
Self::Ksh => {
|
|
// On Ksh it's standard POSIX `.profile` for login shells, and `.kshrc` for non-login.
|
|
vec![home_dir.join(".profile"), home_dir.join(".kshrc")]
|
|
}
|
|
Self::Zsh => {
|
|
// On Zsh, we only need to update `.zshenv`. This file is sourced for both login and
|
|
// non-login shells. However, we match rustup's logic for determining _which_
|
|
// `.zshenv` to use.
|
|
//
|
|
// See: https://github.com/rust-lang/rustup/blob/fede22fea7b160868cece632bd213e6d72f8912f/src/cli/self_update/shell.rs#L197
|
|
let zsh_dot_dir = std::env::var(EnvVars::ZDOTDIR)
|
|
.ok()
|
|
.filter(|dir| !dir.is_empty())
|
|
.map(PathBuf::from);
|
|
|
|
// Attempt to update an existing `.zshenv` file.
|
|
if let Some(zsh_dot_dir) = zsh_dot_dir.as_ref() {
|
|
// If `ZDOTDIR` is set, and `ZDOTDIR/.zshenv` exists, then we update that file.
|
|
let zshenv = zsh_dot_dir.join(".zshenv");
|
|
if zshenv.is_file() {
|
|
return vec![zshenv];
|
|
}
|
|
} else {
|
|
// If `ZDOTDIR` is _not_ set, and `~/.zshenv` exists, then we update that file.
|
|
let zshenv = home_dir.join(".zshenv");
|
|
if zshenv.is_file() {
|
|
return vec![zshenv];
|
|
}
|
|
}
|
|
|
|
if let Some(zsh_dot_dir) = zsh_dot_dir.as_ref() {
|
|
// If `ZDOTDIR` is set, then we create `ZDOTDIR/.zshenv`.
|
|
vec![zsh_dot_dir.join(".zshenv")]
|
|
} else {
|
|
// If `ZDOTDIR` is _not_ set, then we create `~/.zshenv`.
|
|
vec![home_dir.join(".zshenv")]
|
|
}
|
|
}
|
|
Self::Fish => {
|
|
// On Fish, we only need to update `config.fish`. This file is sourced for both
|
|
// login and non-login shells. However, we must respect Fish's logic, which reads
|
|
// from `$XDG_CONFIG_HOME/fish/config.fish` if set, and `~/.config/fish/config.fish`
|
|
// otherwise.
|
|
if let Some(xdg_home_dir) = std::env::var(EnvVars::XDG_CONFIG_HOME)
|
|
.ok()
|
|
.filter(|dir| !dir.is_empty())
|
|
.map(PathBuf::from)
|
|
{
|
|
vec![xdg_home_dir.join("fish/config.fish")]
|
|
} else {
|
|
vec![home_dir.join(".config/fish/config.fish")]
|
|
}
|
|
}
|
|
Self::Csh => {
|
|
// On Csh, we need to update both `.cshrc` and `.login`, like Bash.
|
|
vec![home_dir.join(".cshrc"), home_dir.join(".login")]
|
|
}
|
|
// TODO(charlie): Add support for Nushell.
|
|
Self::Nushell => vec![],
|
|
// See: [`crate::windows::prepend_path`].
|
|
Self::Powershell => vec![],
|
|
// See: [`crate::windows::prepend_path`].
|
|
Self::Cmd => vec![],
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if the given path is on the `PATH` in this shell.
|
|
pub fn contains_path(path: &Path) -> bool {
|
|
let home_dir = home::home_dir();
|
|
std::env::var_os(EnvVars::PATH)
|
|
.as_ref()
|
|
.iter()
|
|
.flat_map(std::env::split_paths)
|
|
.map(|path| {
|
|
// If the first component is `~`, expand to the home directory.
|
|
if let Some(home_dir) = home_dir.as_ref() {
|
|
if path
|
|
.components()
|
|
.next()
|
|
.map(std::path::Component::as_os_str)
|
|
== Some("~".as_ref())
|
|
{
|
|
return home_dir.join(path.components().skip(1).collect::<PathBuf>());
|
|
}
|
|
}
|
|
path
|
|
})
|
|
.any(|p| same_file::is_same_file(path, p).unwrap_or(false))
|
|
}
|
|
|
|
/// Returns the command necessary to prepend a directory to the `PATH` in this shell.
|
|
pub fn prepend_path(self, path: &Path) -> Option<String> {
|
|
match self {
|
|
Self::Nushell => None,
|
|
Self::Bash | Self::Zsh | Self::Ksh => Some(format!(
|
|
"export PATH=\"{}:$PATH\"",
|
|
backslash_escape(&path.simplified_display().to_string()),
|
|
)),
|
|
Self::Fish => Some(format!(
|
|
"fish_add_path \"{}\"",
|
|
backslash_escape(&path.simplified_display().to_string()),
|
|
)),
|
|
Self::Csh => Some(format!(
|
|
"setenv PATH \"{}:$PATH\"",
|
|
backslash_escape(&path.simplified_display().to_string()),
|
|
)),
|
|
Self::Powershell => Some(format!(
|
|
"$env:PATH = \"{};$env:PATH\"",
|
|
backtick_escape(&path.simplified_display().to_string()),
|
|
)),
|
|
Self::Cmd => Some(format!(
|
|
"set PATH=\"{};%PATH%\"",
|
|
backslash_escape(&path.simplified_display().to_string()),
|
|
)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for Shell {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Bash => write!(f, "Bash"),
|
|
Self::Fish => write!(f, "Fish"),
|
|
Self::Powershell => write!(f, "PowerShell"),
|
|
Self::Cmd => write!(f, "Command Prompt"),
|
|
Self::Zsh => write!(f, "Zsh"),
|
|
Self::Nushell => write!(f, "Nushell"),
|
|
Self::Csh => write!(f, "Csh"),
|
|
Self::Ksh => write!(f, "Ksh"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parse the shell from the name of the shell executable.
|
|
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),
|
|
"ksh" => Some(Shell::Ksh),
|
|
"powershell" | "powershell_ise" => Some(Shell::Powershell),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Escape a string for use in a shell command by inserting backslashes.
|
|
fn backslash_escape(s: &str) -> String {
|
|
let mut escaped = String::with_capacity(s.len());
|
|
for c in s.chars() {
|
|
match c {
|
|
'\\' | '"' => escaped.push('\\'),
|
|
_ => {}
|
|
}
|
|
escaped.push(c);
|
|
}
|
|
escaped
|
|
}
|
|
|
|
/// Escape a string for use in a `PowerShell` command by inserting backticks.
|
|
fn backtick_escape(s: &str) -> String {
|
|
let mut escaped = String::with_capacity(s.len());
|
|
for c in s.chars() {
|
|
match c {
|
|
'\\' | '"' | '$' => escaped.push('`'),
|
|
_ => {}
|
|
}
|
|
escaped.push(c);
|
|
}
|
|
escaped
|
|
}
|