Skip to main content

uv_shell/
shlex.rs

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