test(cli): the terminal helpers quote each word (a macOS root has a space)

The portable macOS job: the command line handed to `script` was the
words joined by spaces, and `Library/Application Support/wanderbooks`
split into two arguments ("unknown argument: Support/wanderbooks").
Each word is single-quoted now, on every OS; a word that is a shell
operator a test means as one (the consent test's `>`) stays bare.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
swaitsandClaude Fable 5.1 committed 2026-10-29 19:05:41 -06:00
1 parent a8ce2e7d6c
commit 59ae40aac5
3 files changed
+62 -9

No files matched your search

+22 -4
View File
@@ -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::<Vec<_>>()
.join(" ");
let command = shell_join(
&std::iter::once(env!("CARGO_BIN_EXE_wanderbooks").to_string())
.chain(words.iter().cloned())
.collect::<Vec<_>>(),
);
let mut child = Command::new("script")
.args(script_args(&command))
.env_clear()
@@ -985,3 +986,20 @@ fn script_args(command: &str) -> Vec<String> {
};
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::<Vec<_>>()
.join(" ")
}
+18 -1
View File
@@ -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<String> {
};
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::<Vec<_>>()
.join(" ")
}
+22 -4
View File
@@ -176,10 +176,11 @@ fn script_args(command: &str) -> Vec<String> {
/// 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::<Vec<_>>()
.join(" ");
let command = shell_join(
&std::iter::once(env!("CARGO_BIN_EXE_wanderbooks").to_string())
.chain(args.iter().cloned())
.collect::<Vec<_>>(),
);
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::<Vec<_>>()
.join(" ")
}