pub fn quote(s: &str) -> String {
if !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric() || "-_./=:@,+%".contains(c)) {
return s.to_string();
}
quote_always(s)
}
pub fn quote_always(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
}
pub fn sh_c(script: &str) -> String {
format!("sh -c 'eval \"$(printf \"{}\")\"'", printf_escape(script, true))
}
pub fn printf_escape(s: &str, spaces: bool) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
if b.is_ascii_alphanumeric() || b"-_./:=,+@#()[]{}<>;&|*?~^".contains(&b) || (spaces && b == b' ') {
out.push(b as char);
} else {
out.push_str(&format!("\\{b:03o}"));
}
}
out
}
#[cfg(test)]
pub fn unwrap_sh_c(line: &str) -> String {
let inner = line.strip_prefix("sh -c 'eval \"$(printf \"").and_then(|l| l.strip_suffix("\")\"'"));
let inner = inner.expect("an sh -c line").as_bytes();
let mut out = Vec::new();
let mut i = 0;
while i < inner.len() {
if inner[i] == b'\\' {
let octal = std::str::from_utf8(&inner[i + 1..i + 4]).unwrap();
out.push(u8::from_str_radix(octal, 8).unwrap());
i += 4;
} else {
out.push(inner[i]);
i += 1;
}
}
String::from_utf8(out).unwrap()
}
pub fn join(words: &[String]) -> String {
words.iter().map(|w| quote(w)).collect::<Vec<_>>().join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quoting() {
assert_eq!(quote("plain-path/a.b"), "plain-path/a.b");
assert_eq!(quote(""), "''");
assert_eq!(quote("it's"), "'it'\\''s'");
assert_eq!(quote("$HOME"), "'$HOME'");
assert_eq!(quote_always("x"), "'x'");
assert_eq!(join(&["echo".into(), "a b".into()]), "echo 'a b'");
let script = "x=$(echo 'a b'); for w in $x; do printf '[%s]' \"$w\"; done";
let out = std::process::Command::new("sh").arg("-c").arg(sh_c(script)).output().unwrap();
assert_eq!(String::from_utf8_lossy(&out.stdout), "[a][b]");
assert_eq!(unwrap_sh_c(&sh_c(script)), script);
for s in ["it's", "a\"b", "$(x) `y` \\z", "new\nline", ""] {
let out =
std::process::Command::new("sh").arg("-c").arg(format!("printf %s {}", quote(s))).output().unwrap();
assert_eq!(String::from_utf8_lossy(&out.stdout), s);
}
}
#[test]
fn a_script_reaches_sh_whole_through_any_login_shell() {
let name = "a\\\\b \\' \"q\" $(x) `y` !1 %s\n\té.txt";
let script = format!("printf '%s' {}; printf '|%s' \"$0\"", quote(name));
let line = sh_c(&script);
let inner = &line["sh -c '".len()..line.len() - 1];
assert!(!inner.contains(['\'', '\n', '!', '%']), "{line}");
assert!(!inner.contains("\\\\"), "{line}");
assert!(inner.split('\\').skip(1).all(|p| p.starts_with(|c: char| c.is_ascii_digit())), "{line}");
assert_eq!(unwrap_sh_c(&line), script);
let shells: Vec<&str> = ["sh", "bash", "zsh", "fish", "tcsh", "csh"].into_iter().filter(|s| have(s)).collect();
assert!(shells.contains(&"sh"));
for shell in shells {
let out = std::process::Command::new(shell).arg("-c").arg(&line).output().unwrap();
assert_eq!(String::from_utf8_lossy(&out.stdout), format!("{name}|sh"), "through {shell}");
}
}
fn have(program: &str) -> bool {
std::process::Command::new("sh")
.arg("-c")
.arg(format!("command -v {program} >/dev/null"))
.status()
.is_ok_and(|s| s.success())
}
}