#![cfg(unix)]
use command_stream::cmd;
use command_stream::quote::quote;
const VALUES: &[&str] = &[
"/Users/john/My Documents/report.txt",
"/tmp/two spaces/file.txt",
" /tmp/leading and trailing ",
"/tmp/it's a dir/file.txt",
"/tmp/say \"hi\"/file.txt",
"'/tmp/pre single quoted/file.txt'",
"\"/tmp/pre double quoted/file.txt\"",
"/tmp/$HOME dir/file.txt",
"/tmp/back`tick`/file.txt",
"/tmp/semi;colon/file.txt",
"/tmp/pipe|and&/file.txt",
"/tmp/star*glob?/file.txt",
"/tmp/paren(s)/file.txt",
"C:\\Program Files\\App\\app.exe",
"/tmp/new\nline/file.txt",
];
fn sh_stdout(script: &str, value: Option<&str>) -> String {
let mut command = std::process::Command::new("/bin/sh");
command.arg("-c").arg(script);
if let Some(value) = value {
command.env("V", value);
}
let output = command.output().expect("failed to run /bin/sh");
String::from_utf8_lossy(&output.stdout).into_owned()
}
const SCRIPTS: &[(&str, &str)] = &[
("printf '%s\\n' {}", "printf '%s\\n' \"$V\""),
("printf '%s\\n' {} tail", "printf '%s\\n' \"$V\" tail"),
("printf '[%s]\\n' {}", "printf '[%s]\\n' \"$V\""),
];
#[test]
fn interpolated_values_match_a_quoted_sh_variable() {
for value in VALUES {
for (template, reference) in SCRIPTS {
let built = template.replace("{}", "e(value));
assert_eq!(
sh_stdout(&built, None),
sh_stdout(reference, Some(value)),
"value {value:?} in script {template:?} built as {built:?}"
);
}
}
}
#[tokio::test]
async fn a_path_with_spaces_stays_one_argument() {
let dir = tempfile::Builder::new()
.prefix("my documents ")
.tempdir()
.unwrap();
let file = dir.path().join("annual report 2026.txt");
std::fs::write(&file, "hello content\n").unwrap();
let file = file.to_str().unwrap();
let result = cmd!("cat {}", file).await.unwrap();
assert!(result.is_success(), "stderr: {}", result.stderr);
assert_eq!(result.stdout, "hello content\n");
}
#[tokio::test]
async fn copying_between_directories_with_spaces_works() {
let dir = tempfile::Builder::new()
.prefix("my documents ")
.tempdir()
.unwrap();
let source = dir.path().join("source file.txt");
let target = dir.path().join("target file.txt");
std::fs::write(&source, "copy me\n").unwrap();
let result = cmd!(
"cp {} {}",
source.to_str().unwrap(),
target.to_str().unwrap()
)
.await
.unwrap();
assert!(result.is_success(), "stderr: {}", result.stderr);
assert_eq!(std::fs::read_to_string(&target).unwrap(), "copy me\n");
}
#[tokio::test]
async fn a_pre_quoted_path_is_not_reinterpreted_as_shell_syntax() {
let dir = tempfile::Builder::new()
.prefix("my documents ")
.tempdir()
.unwrap();
let file = dir.path().join("report.txt");
std::fs::write(&file, "hello\n").unwrap();
let pre_quoted = format!("'{}'", file.to_str().unwrap());
let result = cmd!("cat {}", pre_quoted).await.unwrap();
assert!(!result.is_success());
assert!(result.stdout.is_empty(), "stdout: {}", result.stdout);
}
#[tokio::test]
async fn an_interpolated_value_cannot_start_a_second_command() {
let dir = tempfile::Builder::new()
.prefix("injection ")
.tempdir()
.unwrap();
let marker = dir.path().join("pwned");
let evil = format!("' ; touch {} ; '", marker.to_str().unwrap());
let result = cmd!("printf '%s\\n' {}", evil).await.unwrap();
assert!(result.is_success(), "stderr: {}", result.stderr);
assert_eq!(result.stdout, format!("{evil}\n"));
assert!(!marker.exists(), "the injected command ran");
}