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-platform-tags",
|
||||
"uv-pypi-types",
|
||||
"uv-shell",
|
||||
"uv-trampoline-builder",
|
||||
"uv-warnings",
|
||||
"walkdir",
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ tempfile = { version = "3.12.0" }
|
|||
textwrap = { version = "0.16.1" }
|
||||
thiserror = { version = "1.0.63" }
|
||||
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-util = { version = "0.7.12", features = ["compat"] }
|
||||
toml = { version = "0.8.19" }
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ uv-normalize = { workspace = true }
|
|||
uv-pep440 = { workspace = true }
|
||||
uv-platform-tags = { workspace = true }
|
||||
uv-pypi-types = { workspace = true }
|
||||
uv-shell = { workspace = true }
|
||||
uv-trampoline-builder = { workspace = true }
|
||||
uv-warnings = { workspace = true }
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use uv_cache_info::CacheInfo;
|
|||
use uv_fs::{relative_to, Simplified};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pypi_types::DirectUrl;
|
||||
use uv_shell::escape_posix_for_single_quotes;
|
||||
use uv_trampoline_builder::windows_script_launcher;
|
||||
|
||||
use crate::record::RecordEntry;
|
||||
|
|
@ -122,10 +123,11 @@ fn format_shebang(executable: impl AsRef<Path>, os_name: &str, relocatable: bool
|
|||
} else {
|
||||
""
|
||||
};
|
||||
// Like Python's `shlex.quote`:
|
||||
// > Use single quotes, and put single quotes into double quotes
|
||||
// > The string $'b is then quoted as '$'"'"'b'
|
||||
let executable = format!("{}'{}'", prefix, executable.replace('\'', r#"'"'"'"#));
|
||||
let executable = format!(
|
||||
"{}'{}'",
|
||||
prefix,
|
||||
escape_posix_for_single_quotes(&executable)
|
||||
);
|
||||
return format!("#!/bin/sh\n'''exec' {executable} \"$0\" \"$@\"\n' '''");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -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_settings::PythonInstallMirrors;
|
||||
use uv_shell::Shell;
|
||||
use uv_shell::{shlex_posix, shlex_windows, Shell};
|
||||
use uv_types::{BuildContext, BuildIsolation, HashStrategy};
|
||||
use uv_warnings::{warn_user, warn_user_once};
|
||||
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError};
|
||||
|
|
@ -421,37 +421,3 @@ async fn venv_impl(
|
|||
|
||||
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