Skip to main content

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}