1use crate::prelude::{anyhow, Context, Result};
2use flume::{unbounded, Sender};
3use itertools::Itertools;
4use std::ffi::OsStr;
5use std::io::{Read, Write};
6use std::path::Path;
7use std::process::*;
8
9#[derive(Copy, Clone, Default)]
11pub enum Output {
12 Quiet,
14 Stdout,
16 Stderr,
18 #[default]
20 Verbose,
21}
22
23pub trait CommandExecute {
30 fn execute(self, output: Output) -> Result<Vec<u8>>;
32
33 fn execute_str(self, output: Output) -> Result<String>
35 where
36 Self: CommandString + Sized,
37 {
38 let cstr = self.cmd_str();
39 self.execute(output).and_then(|x| {
40 String::from_utf8(x)
41 .context("failed to encode stdout to UTF8 string")
42 .with_context(|| format!("cmd str: {cstr}"))
43 })
44 }
45
46 fn run(self) -> Result<()>;
48}
49
50impl CommandExecute for Command {
76 fn execute(mut self, output: Output) -> Result<Vec<u8>> {
77 let mut child = self
79 .stdout(Stdio::piped())
80 .stderr(Stdio::piped())
81 .spawn()
82 .with_context(|| format!("failed to start cmd: {}", self.cmd_str()))?;
83
84 let stdout = child.stdout.take().expect("stdout piped");
85 let stderr = child.stderr.take().expect("stderr piped");
86
87 let (tx_so, rx_so) = unbounded();
88 let (tx_se, rx_se) = unbounded();
89
90 fn fwd(
91 tx: Sender<Vec<u8>>,
92 mut rdr: impl Read + Send + 'static,
93 print: impl Fn(&[u8]) + Send + 'static,
94 ) {
95 std::thread::spawn(move || {
96 let buf: &mut [u8] = &mut *Box::new([0u8; 1024 * 4]);
97 while let Ok(len) = rdr.read(buf) {
98 if len == 0 {
99 break;
100 }
101
102 let buf = buf[..len].to_vec();
103 print(&buf);
104 let _ = tx.send(buf);
105 }
106 });
107 }
108
109 fwd(tx_so, stdout, move |buf| {
110 if matches!(output, Output::Verbose | Output::Stdout) {
111 let _ = std::io::stdout().write_all(buf);
112 }
113 });
114 fwd(tx_se, stderr, move |buf| {
115 if matches!(output, Output::Verbose | Output::Stderr) {
116 let _ = std::io::stderr().write_all(buf);
117 }
118 });
119
120 let xs = child
121 .wait()
122 .with_context(|| format!("failed to execute cmd: {}", self.cmd_str()))?;
123
124 if xs.success() {
125 Ok(rx_so.into_iter().flatten().collect_vec())
126 } else {
127 let se = rx_se.into_iter().flatten().collect_vec();
128 let se = String::from_utf8_lossy(&se).to_string();
129 Err(anyhow!(se)).with_context(|| format!("failed to execute cmd: {}", self.cmd_str(),))
130 }
131 }
132
133 fn run(mut self) -> Result<()> {
139 self.status().map_err(anyhow::Error::from).and_then(|x| {
140 if x.success() {
141 Ok(())
142 } else {
143 Err(anyhow!("cmd exited with code {}: {}", x, self.cmd_str()))
144 }
145 })
146 }
147}
148
149pub trait CommandBuilder {
162 fn with_arg<S: AsRef<OsStr>>(self, arg: S) -> Self;
164 fn with_args<I, S>(mut self, args: I) -> Self
166 where
167 Self: Sized,
168 I: IntoIterator<Item = S>,
169 S: AsRef<OsStr>,
170 {
171 for a in args {
172 self = self.with_arg(a);
173 }
174 self
175 }
176
177 fn maybe_with_arg<S>(self, apply: bool, arg: S) -> Self
179 where
180 Self: Sized,
181 S: AsRef<OsStr>,
182 {
183 if apply {
184 self.with_arg(arg)
185 } else {
186 self
187 }
188 }
189
190 fn maybe_with_args<I, S>(self, apply: bool, args: I) -> Self
192 where
193 Self: Sized,
194 I: IntoIterator<Item = S>,
195 S: AsRef<OsStr>,
196 {
197 if apply {
198 self.with_args(args)
199 } else {
200 self
201 }
202 }
203
204 fn with_env<K, V>(self, key: K, val: V) -> Self
206 where
207 K: AsRef<OsStr>,
208 V: AsRef<OsStr>;
209
210 fn with_envs<I, K, V>(mut self, vars: I) -> Self
212 where
213 Self: Sized,
214 I: IntoIterator<Item = (K, V)>,
215 K: AsRef<OsStr>,
216 V: AsRef<OsStr>,
217 {
218 for (k, v) in vars {
219 self = self.with_env(k, v);
220 }
221 self
222 }
223
224 fn with_current_dir<P: AsRef<Path>>(self, path: P) -> Self;
226
227 fn pipe(self, next: Command) -> Result<Self>
229 where
230 Self: Sized;
231
232 fn pipe_stderr(self, next: Command) -> Result<Self>
234 where
235 Self: Sized;
236}
237
238impl CommandBuilder for Command {
239 fn with_arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
240 self.arg(arg);
241 self
242 }
243
244 fn with_env<K, V>(mut self, key: K, val: V) -> Self
245 where
246 K: AsRef<OsStr>,
247 V: AsRef<OsStr>,
248 {
249 self.env(key, val);
250 self
251 }
252
253 fn with_current_dir<P: AsRef<Path>>(mut self, dir: P) -> Self {
254 self.current_dir(dir);
255 self
256 }
257
258 fn pipe(mut self, mut next: Command) -> Result<Self> {
259 let cmd = self
260 .stdout(Stdio::piped())
261 .spawn()
262 .map_err(|e| anyhow!("encountered error with command {}: {e}", self.cmd_str()))?;
263
264 let out = cmd.stdout.expect("piped so should exist");
265 let stdin = Stdio::from(out);
266
267 next.stdin(stdin);
268 Ok(next)
269 }
270
271 fn pipe_stderr(mut self, mut next: Command) -> Result<Self> {
272 let cmd = self
273 .stderr(Stdio::piped())
274 .spawn()
275 .map_err(|e| anyhow!("encountered error with command {}: {e}", self.cmd_str()))?;
276
277 let out = cmd.stderr.expect("piped so should exist");
278 let stdin = Stdio::from(out);
279
280 next.stdin(stdin);
281 Ok(next)
282 }
283}
284
285pub trait CommandString {
287 fn cmd_str(&self) -> String;
289
290 fn debug_print(self) -> Self
292 where
293 Self: Sized,
294 {
295 eprintln!("{}", self.cmd_str());
296 self
297 }
298}
299
300impl CommandString for Command {
301 fn cmd_str(&self) -> String {
302 let x = format!("{self:#?}");
304 let prg = if cfg!(windows) {
307 x.split_once(' ')
308 .map(|x| x.0)
309 .unwrap_or(&x)
310 .trim_matches('"')
311 } else {
312 x.split_once("program:")
313 .expect("known format")
314 .1
315 .split_once(',')
316 .expect("known format")
317 .0
318 .trim()
319 .trim_matches('"')
320 };
321
322 self.get_args()
325 .fold(prg.to_string(), |s, a| s + " " + &*a.to_string_lossy())
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::Output::*;
332 use super::*;
333 use crate::prelude::*;
334 use crate::pretty_print_err;
335 use insta::assert_snapshot;
336
337 #[test]
338 fn cmd_macro_output() {
339 let x = cmd!(ls).cmd_str();
340 assert_eq!(&x, "ls");
341
342 let x = cmd!(ls: foo, bar).cmd_str();
343 assert_eq!(&x, "ls foo bar");
344
345 let x = cmd!(ls: {"foo"}, bar).cmd_str();
346 assert_eq!(&x, "ls foo bar");
347
348 let x = cmd!(ls: "foo bar").cmd_str();
349 assert_eq!(&x, r#"ls "foo bar""#);
350
351 let x = cmd!(./script.sh: "foo bar").cmd_str();
352 assert_eq!(&x, r#"./script.sh "foo bar""#);
353 }
354
355 #[test]
356 fn cmd_execute() {
357 let x = cmd!(ls).execute_str(Quiet).unwrap();
358 let mut x = x.trim().split('\n').collect::<Vec<_>>();
359 x.sort();
360
361 assert_eq!(
362 &x,
363 &[
364 "Cargo.lock",
365 "Cargo.toml",
366 "LICENSE",
367 "README.md",
368 "macros",
369 "src",
370 "target",
371 "template-cargo-script.rs",
372 "template-rust-script.rs",
373 ]
374 );
375
376 let x = cmd!(ls: "foo").execute_str(Verbose).unwrap_err();
377 assert_snapshot!("execute-err", pretty_print_err(x));
378
379 let x = cmd!(watcmd: "foo").execute_str(Verbose).unwrap_err();
380 assert_snapshot!("unknown-cmd", pretty_print_err(x));
381 }
382
383 #[test]
384 fn cmd_naming_with_env() {
385 let x = cmd!(ls).with_env("YO", "zog").cmd_str();
386 assert_eq!(&x, "ls");
387
388 let x = cmd!(ls: foo, bar).with_env("YO", "zog").cmd_str();
389 assert_eq!(&x, "ls foo bar");
390
391 let x = cmd!(ls: foo, bar)
392 .with_envs([("YO", "zog"), ("JO", "bar")])
393 .cmd_str();
394 assert_eq!(&x, "ls foo bar");
395 }
396
397 #[test]
398 fn cmd_piping() {
399 let x = cmd!(ls)
400 .pipe(cmd!(grep: Cargo.*))
401 .unwrap()
402 .execute_str(Quiet)
403 .unwrap();
404 let mut x = x.trim().split('\n').collect::<Vec<_>>();
405 x.sort();
406
407 assert_eq!(&x, &["Cargo.lock", "Cargo.toml",]);
408
409 let x = cmd!(ls)
410 .pipe(cmd!(grep: Cargo.*))
411 .unwrap()
412 .pipe(cmd!(grep: toml))
413 .unwrap()
414 .execute_str(Quiet)
415 .unwrap();
416 let mut x = x.trim().split('\n').collect::<Vec<_>>();
417 x.sort();
418
419 assert_eq!(&x, &["Cargo.toml",]);
420
421 let x = cmd!(ls: foo)
422 .pipe_stderr(cmd!(grep: foo))
423 .unwrap()
424 .execute_str(Quiet)
425 .unwrap();
426 let mut x = x.trim().split('\n').collect::<Vec<_>>();
427 x.sort();
428
429 assert_eq!(&x, &["ls: cannot access 'foo': No such file or directory",]);
430 }
431}