1use std::io::Write;
10use std::process::{Command, Stdio};
11
12use crate::error::{DogeError, DogeResult};
13use crate::ordered_map::OrderedMap;
14use crate::stdlib::str_arg;
15use crate::value::Value;
16
17const SIGNAL_EXIT_CODE: i64 = -1;
20
21pub fn chase_run(cmd: &Value, args: &Value, stdin: &Value) -> DogeResult {
26 let cmd = str_arg("chase", "run", cmd)?;
27 let args = arg_list(args)?;
28 let stdin = stdin_text(stdin)?;
29
30 let mut command = Command::new(cmd);
31 command.args(&args);
32 command.stdout(Stdio::piped());
33 command.stderr(Stdio::piped());
34 command.stdin(match stdin {
35 Some(_) => Stdio::piped(),
36 None => Stdio::null(),
37 });
38
39 let mut child = command
40 .spawn()
41 .map_err(|err| DogeError::io_error(format!("cannot run {cmd}: {err}")))?;
42
43 let writer = stdin.map(|text| {
49 let mut handle = child.stdin.take();
50 std::thread::spawn(move || {
51 if let Some(pipe) = handle.as_mut() {
52 let _ = pipe.write_all(text.as_bytes());
53 }
54 })
55 });
56
57 let output = child
58 .wait_with_output()
59 .map_err(|err| DogeError::io_error(format!("cannot run {cmd}: {err}")))?;
60 if let Some(writer) = writer {
61 let _ = writer.join();
62 }
63
64 let code = output.status.code().map_or(SIGNAL_EXIT_CODE, i64::from);
65 let stdout = decode(cmd, "stdout", output.stdout)?;
66 let stderr = decode(cmd, "stderr", output.stderr)?;
67
68 let mut result = OrderedMap::new();
69 result.insert("code".to_string(), Value::int(code));
70 result.insert("stdout".to_string(), Value::str(stdout));
71 result.insert("stderr".to_string(), Value::str(stderr));
72 Ok(Value::dict(result))
73}
74
75fn arg_list(args: &Value) -> DogeResult<Vec<String>> {
78 let items = match args {
79 Value::List(items) => items.borrow(),
80 _ => {
81 return Err(DogeError::type_error(format!(
82 "chase.run needs a List of Str for its args, got {}",
83 args.describe()
84 )))
85 }
86 };
87 let mut out = Vec::with_capacity(items.len());
88 for item in items.iter() {
89 match item {
90 Value::Str(s) => out.push(s.to_string()),
91 other => {
92 return Err(DogeError::type_error(format!(
93 "chase.run needs a List of Str for its args, got a {} element",
94 other.describe()
95 )))
96 }
97 }
98 }
99 Ok(out)
100}
101
102fn stdin_text(stdin: &Value) -> DogeResult<Option<String>> {
105 match stdin {
106 Value::Str(s) => Ok(Some(s.to_string())),
107 Value::None => Ok(None),
108 other => Err(DogeError::type_error(format!(
109 "chase.run needs a Str or none for its stdin, got {}",
110 other.describe()
111 ))),
112 }
113}
114
115fn decode(cmd: &str, stream: &str, bytes: Vec<u8>) -> DogeResult<String> {
118 String::from_utf8(bytes)
119 .map_err(|_| DogeError::io_error(format!("{cmd} wrote non-text bytes to {stream}")))
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::error::ErrorKind;
126 use bigdecimal::ToPrimitive;
127
128 fn run(cmd: &str, args: &[&str], stdin: Value) -> DogeResult {
129 let args = Value::list(args.iter().map(Value::str).collect());
130 chase_run(&Value::str(cmd), &args, &stdin)
131 }
132
133 fn assert_str(dict: &Value, key: &str, expected: &str) {
136 match dict {
137 Value::Dict(entries) => match entries.borrow().get(key) {
138 Some(Value::Str(s)) => assert_eq!(&**s, expected, "{key}"),
139 other => panic!("expected a Str {key}, got {other:?}"),
140 },
141 _ => panic!("expected a dict"),
142 }
143 }
144
145 fn assert_code(dict: &Value, expected: i64) {
146 match dict {
147 Value::Dict(entries) => match entries.borrow().get("code") {
148 Some(Value::Int(n)) => assert_eq!(n.to_i64().unwrap(), expected, "code"),
149 other => panic!("expected an Int code, got {other:?}"),
150 },
151 _ => panic!("expected a dict"),
152 }
153 }
154
155 #[test]
156 fn captures_stdout_and_a_zero_code() {
157 let out = run("printf", &["much wow"], Value::None).unwrap();
158 assert_code(&out, 0);
159 assert_str(&out, "stdout", "much wow");
160 assert_str(&out, "stderr", "");
161 }
162
163 #[test]
164 fn passes_arguments_through() {
165 let out = run("printf", &["%s-%s", "such", "wow"], Value::None).unwrap();
166 assert_str(&out, "stdout", "such-wow");
167 }
168
169 #[test]
170 fn feeds_stdin_to_the_child() {
171 let out = run("cat", &[], Value::str("such stdin")).unwrap();
172 assert_str(&out, "stdout", "such stdin");
173 }
174
175 #[test]
176 fn a_child_that_ignores_stdin_does_not_hang_or_error() {
177 let out = run("true", &[], Value::str("wasted input")).unwrap();
178 assert_code(&out, 0);
179 }
180
181 #[test]
182 fn reports_a_nonzero_exit_code() {
183 let out = run("false", &[], Value::None).unwrap();
184 assert_code(&out, 1);
185 }
186
187 #[test]
188 fn a_missing_program_is_a_catchable_io_error() {
189 let err = run("doge-no-such-prog-xyz", &[], Value::None).unwrap_err();
190 assert_eq!(err.kind, ErrorKind::IOError);
191 }
192
193 #[test]
194 fn a_non_str_command_is_a_type_error() {
195 let err = chase_run(&Value::int(1), &Value::list(vec![]), &Value::None).unwrap_err();
196 assert_eq!(err.kind, ErrorKind::TypeError);
197 }
198
199 #[test]
200 fn non_list_args_is_a_type_error() {
201 let err = chase_run(&Value::str("echo"), &Value::int(1), &Value::None).unwrap_err();
202 assert_eq!(err.kind, ErrorKind::TypeError);
203 }
204
205 #[test]
206 fn a_non_str_args_element_is_a_type_error() {
207 let args = Value::list(vec![Value::int(1)]);
208 let err = chase_run(&Value::str("echo"), &args, &Value::None).unwrap_err();
209 assert_eq!(err.kind, ErrorKind::TypeError);
210 }
211
212 #[test]
213 fn a_non_str_non_none_stdin_is_a_type_error() {
214 let err = chase_run(&Value::str("cat"), &Value::list(vec![]), &Value::int(1)).unwrap_err();
215 assert_eq!(err.kind, ErrorKind::TypeError);
216 }
217}