diff --git a/crates/uv-shell/src/shlex.rs b/crates/uv-shell/src/shlex.rs index cdcbd478ffe77..b53b806939e60 100644 --- a/crates/uv-shell/src/shlex.rs +++ b/crates/uv-shell/src/shlex.rs @@ -6,13 +6,15 @@ pub fn shlex_posix(executable: impl AsRef) -> String { // Convert to a display path. let executable = executable.as_ref().portable_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!("'{}'", escape_posix_for_single_quotes(&executable)) - } else { + // Match Python's `shlex.quote` and leave only shell-safe ASCII characters unquoted. + if !executable.is_empty() + && executable + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || b"@%+=:,./-_".contains(&byte)) + { executable + } else { + format!("'{}'", escape_posix_for_single_quotes(&executable)) } } @@ -48,3 +50,26 @@ pub fn shlex_windows(executable: impl AsRef, shell: Shell) -> String { executable } } + +#[cfg(test)] +mod tests { + use super::shlex_posix; + + #[test] + fn posix_safe_path() { + assert_eq!(shlex_posix("/usr/bin/python3.12"), "/usr/bin/python3.12"); + } + + #[test] + fn posix_empty_path() { + assert_eq!(shlex_posix(""), "''"); + } + + #[test] + fn posix_path_with_metacharacters() { + assert_eq!( + shlex_posix("Testing's/$venv;activate"), + r#"'Testing'"'"'s/$venv;activate'"# + ); + } +} diff --git a/crates/uv/tests/python/venv.rs b/crates/uv/tests/python/venv.rs index 5dc1cd3d92302..4bbea40c17674 100644 --- a/crates/uv/tests/python/venv.rs +++ b/crates/uv/tests/python/venv.rs @@ -1610,7 +1610,7 @@ fn create_venv_apostrophe() { ----- stderr ----- Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: Testing's - Activate with: source Testing's/[BIN]/activate + Activate with: source 'Testing'\"'\"'s/[BIN]/activate' " );