mirror of https://github.com/astral-sh/uv
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:
parent
7b4197bc0e
commit
0abb2a4595
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue