pub const WRAPPER_PREFIX_LINES: usize = 1;
pub fn wrap_script_stdin_isolated(script: &str) -> String {
let body = if script.trim().is_empty() {
":"
} else {
script.trim_end()
};
format!("{{\n{body}\n}} < /dev/null\n")
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::process::{Command, Stdio};
fn run_via_stdin(script: &str) -> (String, Option<i32>) {
let mut child = Command::new("bash")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("bash");
child
.stdin
.as_mut()
.unwrap()
.write_all(script.as_bytes())
.unwrap();
let out = child.wait_with_output().unwrap();
(
String::from_utf8_lossy(&out.stdout).to_string(),
out.status.code(),
)
}
fn tmp(tag: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("forjar-stdin-{}-{}", tag, std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
#[test]
fn unwrapped_script_is_eaten_by_a_stdin_reading_command() {
let d = tmp("defect");
let script = format!(
"cd {}\ncat > eaten.txt\necho SECOND > second.txt\n",
d.display()
);
run_via_stdin(&script);
assert!(
!d.join("second.txt").exists(),
"the defect requires line 2 to be swallowed"
);
let eaten = std::fs::read_to_string(d.join("eaten.txt")).unwrap_or_default();
assert!(
eaten.contains("echo SECOND"),
"line 2 became line 1's input"
);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn wrapped_script_runs_every_line() {
let d = tmp("fixed");
let script = format!(
"cd {}\ncat > eaten.txt\necho SECOND > second.txt\n",
d.display()
);
run_via_stdin(&wrap_script_stdin_isolated(&script));
assert!(
d.join("second.txt").exists(),
"line 2 must run once the script is not its own stdin"
);
assert_eq!(
std::fs::read_to_string(d.join("second.txt"))
.unwrap()
.trim(),
"SECOND"
);
assert_eq!(
std::fs::read_to_string(d.join("eaten.txt")).unwrap(),
"",
"the stdin-reading command gets /dev/null, not the script"
);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn exit_status_still_propagates() {
assert_eq!(
run_via_stdin(&wrap_script_stdin_isolated("exit 7")).1,
Some(7)
);
assert_eq!(
run_via_stdin(&wrap_script_stdin_isolated("set -e\nfalse\necho NOPE")).1,
Some(1)
);
assert_eq!(
run_via_stdin(&wrap_script_stdin_isolated("true")).1,
Some(0)
);
}
#[test]
fn stdout_is_unchanged() {
let (out, code) = run_via_stdin(&wrap_script_stdin_isolated("echo one\necho two"));
assert_eq!(out, "one\ntwo\n");
assert_eq!(code, Some(0));
}
#[test]
fn a_heredoc_inside_the_script_still_works() {
let d = tmp("heredoc");
let script = format!(
"cat > {}/h.txt <<'EOF'\nline one\nline two\nEOF\n",
d.display()
);
let (_, code) = run_via_stdin(&wrap_script_stdin_isolated(&script));
assert_eq!(code, Some(0));
assert_eq!(
std::fs::read_to_string(d.join("h.txt")).unwrap(),
"line one\nline two\n"
);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn set_euo_pipefail_preamble_survives() {
let s = wrap_script_stdin_isolated("set -euo pipefail\necho ok");
assert_eq!(run_via_stdin(&s).0, "ok\n");
let bad = wrap_script_stdin_isolated("set -euo pipefail\nfalse\necho NOPE");
assert_eq!(run_via_stdin(&bad).1, Some(1));
}
#[test]
fn an_explicit_input_redirection_still_reaches_the_command() {
let d = tmp("explicit");
std::fs::write(d.join("in.txt"), "REAL INPUT\n").unwrap();
let script = format!(
"cd {}\ncat > got.txt < in.txt\necho done > done.txt\n",
d.display()
);
run_via_stdin(&wrap_script_stdin_isolated(&script));
assert_eq!(
std::fs::read_to_string(d.join("got.txt")).unwrap(),
"REAL INPUT\n"
);
assert!(d.join("done.txt").exists(), "the next line still runs");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn an_empty_script_still_succeeds() {
for s in ["", " ", "\n\n"] {
assert_eq!(
run_via_stdin(&wrap_script_stdin_isolated(s)).1,
Some(0),
"empty script {s:?} must exit 0"
);
}
}
#[test]
fn the_documented_line_offset_is_correct() {
let wrapped = wrap_script_stdin_isolated("echo a\necho b");
assert_eq!(
wrapped.lines().position(|l| l == "echo a").unwrap(),
WRAPPER_PREFIX_LINES,
"a bash `line N` maps to script line N - WRAPPER_PREFIX_LINES"
);
}
}