diff --git a/crates/ledger-cli/tests/format_upgrade.rs b/crates/ledger-cli/tests/format_upgrade.rs index b5a9dba..c5b5d44 100644 --- a/crates/ledger-cli/tests/format_upgrade.rs +++ b/crates/ledger-cli/tests/format_upgrade.rs @@ -139,10 +139,11 @@ impl Home { /// asked for (a prompt ending ": " or "] "): its exit code and what /// the terminal showed. fn at_terminal(&self, words: &[String], lines: &[&str]) -> (i32, String) { - let command = std::iter::once(env!("CARGO_BIN_EXE_wanderbooks").to_string()) - .chain(words.iter().cloned()) - .collect::>() - .join(" "); + let command = shell_join( + &std::iter::once(env!("CARGO_BIN_EXE_wanderbooks").to_string()) + .chain(words.iter().cloned()) + .collect::>(), + ); let mut child = Command::new("script") .args(script_args(&command)) .env_clear() @@ -985,3 +986,20 @@ fn script_args(command: &str) -> Vec { }; args.iter().map(|a| a.to_string()).collect() } + +/// `words` as one shell command line, each word single-quoted (a macOS +/// root has a space in it: `Library/Application Support`), except a +/// word that is a shell operator a test means as one (`>`, `2>&1`, `|`). +fn shell_join(words: &[String]) -> String { + words + .iter() + .map(|w| { + if matches!(w.as_str(), ">" | ">>" | "<" | "|" | "2>&1") { + w.clone() + } else { + format!("'{}'", w.replace('\'', "'\\''")) + } + }) + .collect::>() + .join(" ") +} diff --git a/crates/ledger-cli/tests/open_while_held.rs b/crates/ledger-cli/tests/open_while_held.rs index d1275c8..8bb5c28 100644 --- a/crates/ledger-cli/tests/open_while_held.rs +++ b/crates/ledger-cli/tests/open_while_held.rs @@ -1944,7 +1944,7 @@ fn the_books_command_takes_its_flags_and_asks_before_adopting() { args.push(a.to_string()); } let mut child = Command::new("script") - .args(script_args(&args.join(" "))) + .args(script_args(&shell_join(&args))) .env_clear() .env("PATH", "/usr/bin:/bin") .env("HOME", home.dir.path()) @@ -2960,3 +2960,20 @@ fn script_args(command: &str) -> Vec { }; args.iter().map(|a| a.to_string()).collect() } + +/// `words` as one shell command line, each word single-quoted (a macOS +/// root has a space in it: `Library/Application Support`), except a +/// word that is a shell operator a test means as one (`>`, `2>&1`, `|`). +fn shell_join(words: &[String]) -> String { + words + .iter() + .map(|w| { + if matches!(w.as_str(), ">" | ">>" | "<" | "|" | "2>&1") { + w.clone() + } else { + format!("'{}'", w.replace('\'', "'\\''")) + } + }) + .collect::>() + .join(" ") +} diff --git a/crates/ledger-cli/tests/setup.rs b/crates/ledger-cli/tests/setup.rs index 8e2dddb..e7f5868 100644 --- a/crates/ledger-cli/tests/setup.rs +++ b/crates/ledger-cli/tests/setup.rs @@ -176,10 +176,11 @@ fn script_args(command: &str) -> Vec { /// showed. fn at_terminal(args: &[String], lines: &[&str]) -> (i32, String) { use std::io::Write; - let command = std::iter::once(env!("CARGO_BIN_EXE_wanderbooks").to_string()) - .chain(args.iter().cloned()) - .collect::>() - .join(" "); + let command = shell_join( + &std::iter::once(env!("CARGO_BIN_EXE_wanderbooks").to_string()) + .chain(args.iter().cloned()) + .collect::>(), + ); let mut child = Command::new("script") .args(script_args(&command)) .env_remove("XDG_DATA_HOME") @@ -323,3 +324,20 @@ fn the_passphrase_is_typed_by_a_person_and_changed_only_with_the_one_in_force() let kept = std::fs::read_to_string(auth.join("owner.json")).unwrap(); assert!(!kept.contains("staple"), "the text itself was kept"); } + +/// `words` as one shell command line, each word single-quoted (a macOS +/// root has a space in it: `Library/Application Support`), except a +/// word that is a shell operator a test means as one (`>`, `2>&1`, `|`). +fn shell_join(words: &[String]) -> String { + words + .iter() + .map(|w| { + if matches!(w.as_str(), ">" | ">>" | "<" | "|" | "2>&1") { + w.clone() + } else { + format!("'{}'", w.replace('\'', "'\\''")) + } + }) + .collect::>() + .join(" ") +}