Refactor shell quoting (#9055)

Move the shlex-like quoting utils in the uv-shell crate, so we only
write `r#"'"'"'"#` once.

Split out from #8984
This commit is contained in:
konsti 2024-11-15 10:06:54 +01:00 committed by GitHub
parent 7b4197bc0e
commit 0abb2a4595
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 64 additions and 41 deletions

1
Cargo.lock generated
View File

@ -5025,6 +5025,7 @@ dependencies = [
"uv-pep440", "uv-pep440",
"uv-platform-tags", "uv-platform-tags",
"uv-pypi-types", "uv-pypi-types",
"uv-shell",
"uv-trampoline-builder", "uv-trampoline-builder",
"uv-warnings", "uv-warnings",
"walkdir", "walkdir",

View File

@ -162,7 +162,7 @@ tempfile = { version = "3.12.0" }
textwrap = { version = "0.16.1" } textwrap = { version = "0.16.1" }
thiserror = { version = "1.0.63" } thiserror = { version = "1.0.63" }
tl = { git = "https://github.com/charliermarsh/tl.git", rev = "6e25b2ee2513d75385101a8ff9f591ef51f314ec" } tl = { git = "https://github.com/charliermarsh/tl.git", rev = "6e25b2ee2513d75385101a8ff9f591ef51f314ec" }
tokio = { version = "1.40.0", features = ["fs", "io-util", "macros", "process", "signal", "sync"] } tokio = { version = "1.40.0", features = ["fs", "io-util", "macros", "process", "rt", "signal", "sync"] }
tokio-stream = { version = "0.1.16" } tokio-stream = { version = "0.1.16" }
tokio-util = { version = "0.7.12", features = ["compat"] } tokio-util = { version = "0.7.12", features = ["compat"] }
toml = { version = "0.8.19" } toml = { version = "0.8.19" }

View File

@ -28,6 +28,7 @@ uv-normalize = { workspace = true }
uv-pep440 = { workspace = true } uv-pep440 = { workspace = true }
uv-platform-tags = { workspace = true } uv-platform-tags = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-shell = { workspace = true }
uv-trampoline-builder = { workspace = true } uv-trampoline-builder = { workspace = true }
uv-warnings = { workspace = true } uv-warnings = { workspace = true }

View File

@ -16,6 +16,7 @@ use uv_cache_info::CacheInfo;
use uv_fs::{relative_to, Simplified}; use uv_fs::{relative_to, Simplified};
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pypi_types::DirectUrl; use uv_pypi_types::DirectUrl;
use uv_shell::escape_posix_for_single_quotes;
use uv_trampoline_builder::windows_script_launcher; use uv_trampoline_builder::windows_script_launcher;
use crate::record::RecordEntry; use crate::record::RecordEntry;
@ -122,10 +123,11 @@ fn format_shebang(executable: impl AsRef<Path>, os_name: &str, relocatable: bool
} else { } else {
"" ""
}; };
// Like Python's `shlex.quote`: let executable = format!(
// > Use single quotes, and put single quotes into double quotes "{}'{}'",
// > The string $'b is then quoted as '$'"'"'b' prefix,
let executable = format!("{}'{}'", prefix, executable.replace('\'', r#"'"'"'"#)); escape_posix_for_single_quotes(&executable)
);
return format!("#!/bin/sh\n'''exec' {executable} \"$0\" \"$@\"\n' '''"); return format!("#!/bin/sh\n'''exec' {executable} \"$0\" \"$@\"\n' '''");
} }
} }
@ -769,7 +771,7 @@ mod test {
Wheel-Version: 1.0 Wheel-Version: 1.0
Generator: bdist_wheel (0.37.1) Generator: bdist_wheel (0.37.1)
Root-Is-Purelib: false Root-Is-Purelib: false
Tag: cp38-cp38-manylinux_2_17_x86_64 Tag: cp38-cp38-manylinux_2_17_x86_64
"}; "};
let wheel = parse_email_message_file(&mut text.as_bytes(), "WHEEL").unwrap(); let wheel = parse_email_message_file(&mut text.as_bytes(), "WHEEL").unwrap();

View File

@ -1,5 +1,8 @@
mod shlex;
pub mod windows; pub mod windows;
pub use shlex::{escape_posix_for_single_quotes, shlex_posix, shlex_windows};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_static::EnvVars; use uv_static::EnvVars;

View File

@ -0,0 +1,50 @@
use crate::{Shell, Simplified};
use std::path::Path;
/// Quote a path, if necessary, for safe use in a POSIX-compatible shell command.
pub fn shlex_posix(executable: impl AsRef<Path>) -> 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 {
executable
}
}
/// Escape a string for being used in single quotes in a POSIX-compatible shell command.
///
/// We want our scripts to support any POSIX shell. There's two kind of quotes in POSIX:
/// Single and double quotes. In bash, single quotes must not contain another single
/// quote, you can't even escape it (<https://linux.die.net/man/1/bash> under "QUOTING").
/// Double quotes have escaping rules different from shell to shell, which we can't do.
/// Bash has `$'\''`, but that's not universal enough.
///
/// As solution, use implicit string concatenations, by putting the single quote into double
/// quotes.
pub fn escape_posix_for_single_quotes(string: &str) -> String {
string.replace('\'', r#"'"'"'"#)
}
/// Quote a path, if necessary, for safe use in `PowerShell` and `cmd`.
pub fn shlex_windows(executable: impl AsRef<Path>, shell: Shell) -> String {
// Convert to a display path.
let executable = executable.as_ref().user_display().to_string();
// Wrap the executable in quotes (and a `&` invocation on PowerShell), if it contains spaces.
if executable.contains(' ') {
if shell == Shell::Powershell {
// For PowerShell, wrap in a `&` invocation.
format!("& \"{executable}\"")
} else {
// Otherwise, assume `cmd`, which doesn't need the `&`.
format!("\"{executable}\"")
}
} else {
executable
}
}

View File

@ -25,7 +25,7 @@ use uv_python::{
}; };
use uv_resolver::{ExcludeNewer, FlatIndex}; use uv_resolver::{ExcludeNewer, FlatIndex};
use uv_settings::PythonInstallMirrors; use uv_settings::PythonInstallMirrors;
use uv_shell::Shell; use uv_shell::{shlex_posix, shlex_windows, Shell};
use uv_types::{BuildContext, BuildIsolation, HashStrategy}; use uv_types::{BuildContext, BuildIsolation, HashStrategy};
use uv_warnings::{warn_user, warn_user_once}; use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError}; use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError};
@ -421,37 +421,3 @@ async fn venv_impl(
Ok(ExitStatus::Success) Ok(ExitStatus::Success)
} }
/// Quote a path, if necessary, for safe use in a POSIX-compatible shell command.
fn shlex_posix(executable: impl AsRef<Path>) -> 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!("'{}'", executable.replace('\'', r#"'"'"'"#))
} else {
executable
}
}
/// Quote a path, if necessary, for safe use in `PowerShell` and `cmd`.
fn shlex_windows(executable: impl AsRef<Path>, shell: Shell) -> String {
// Convert to a display path.
let executable = executable.as_ref().user_display().to_string();
// Wrap the executable in quotes (and a `&` invocation on PowerShell), if it contains spaces.
if executable.contains(' ') {
if shell == Shell::Powershell {
// For PowerShell, wrap in a `&` invocation.
format!("& \"{executable}\"")
} else {
// Otherwise, assume `cmd`, which doesn't need the `&`.
format!("\"{executable}\"")
}
} else {
executable
}
}