uv_shell/shlex.rs
1use crate::{Shell, Simplified};
2use std::borrow::Cow;
3use std::path::Path;
4
5/// Quote a path, if necessary, for safe use in a POSIX-compatible shell command.
6pub fn shlex_posix(executable: impl AsRef<Path>) -> String {
7 // Convert to a display path.
8 let executable = executable.as_ref().portable_display().to_string();
9
10 // Match Python's `shlex.quote` and leave only shell-safe ASCII characters unquoted.
11 if !executable.is_empty()
12 && executable
13 .bytes()
14 .all(|byte| byte.is_ascii_alphanumeric() || b"@%+=:,./-_".contains(&byte))
15 {
16 executable
17 } else {
18 format!("'{}'", escape_posix_for_single_quotes(&executable))
19 }
20}
21
22/// Escape a string for being used in single quotes in a POSIX-compatible shell command.
23///
24/// We want our scripts to support any POSIX shell. There are two kinds of quotes in POSIX:
25/// Single and double quotes. In bash, single quotes must not contain another single
26/// quote, you can't even escape it (<https://linux.die.net/man/1/bash> under "QUOTING").
27/// Double quotes have escaping rules that differ from shell to shell, which we can't handle.
28/// Bash has `$'\''`, but that's not universal enough.
29///
30/// As a solution, use implicit string concatenations, by putting the single quote into double
31/// quotes.
32pub fn escape_posix_for_single_quotes(string: &str) -> Cow<'_, str> {
33 if string.contains('\'') {
34 Cow::Owned(string.replace('\'', r#"'"'"'"#))
35 } else {
36 Cow::Borrowed(string)
37 }
38}
39
40/// Quote a path, if necessary, for safe use in `PowerShell` and `cmd`.
41pub fn shlex_windows(executable: impl AsRef<Path>, shell: Shell) -> String {
42 // Convert to a display path.
43 let executable = executable.as_ref().user_display().to_string();
44
45 // Wrap the executable in quotes (and a `&` invocation on PowerShell), if it contains spaces.
46 if executable.contains(' ') {
47 if shell == Shell::Powershell {
48 // For PowerShell, wrap in a `&` invocation.
49 format!("& \"{executable}\"")
50 } else {
51 // Otherwise, assume `cmd`, which doesn't need the `&`.
52 format!("\"{executable}\"")
53 }
54 } else {
55 executable
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::shlex_posix;
62
63 #[test]
64 fn posix_safe_path() {
65 assert_eq!(shlex_posix("/usr/bin/python3.12"), "/usr/bin/python3.12");
66 }
67
68 #[test]
69 fn posix_empty_path() {
70 assert_eq!(shlex_posix(""), "''");
71 }
72
73 #[test]
74 fn posix_path_with_metacharacters() {
75 assert_eq!(
76 shlex_posix("Testing's/$venv;activate"),
77 r#"'Testing'"'"'s/$venv;activate'"#
78 );
79 }
80}