1use std::borrow::Cow;
2use std::ffi::OsStr;
3
4#[derive(Debug, thiserror::Error)]
5#[error("Command execution failed: io_error={io_error:?}, exit_status={exit_status:?}")]
6pub struct CommandError {
7 pub io_error: Option<std::io::Error>,
8 pub exit_status: Option<std::process::ExitStatus>,
9}
10
11fn write_stdin(
12 child: &mut std::process::Child,
13 stdin_data: Option<Vec<u8>>,
14) -> Result<(), CommandError> {
15 use std::io::Write;
16
17 if let Some(data) = stdin_data {
18 child
19 .stdin
20 .take()
21 .unwrap()
22 .write_all(&data)
23 .map_err(|io_error| CommandError {
24 io_error: Some(io_error),
25 exit_status: None,
26 })?;
27 }
28
29 Ok(())
30}
31
32fn run_and_wait(
33 mut child: std::process::Child,
34 stdin_data: Option<Vec<u8>>,
35 start: std::time::Instant,
36) -> Result<std::process::Output, CommandError> {
37 write_stdin(&mut child, stdin_data)?;
38
39 let output = child.wait_with_output().map_err(|io_error| CommandError {
40 io_error: Some(io_error),
41 exit_status: None,
42 })?;
43
44 log::debug!(
45 "exit_status={:?} runtime={:?}",
46 output.status,
47 start.elapsed()
48 );
49
50 Ok(output)
51}
52
53fn run_and_wait_status(
54 mut child: std::process::Child,
55 stdin_data: Option<Vec<u8>>,
56 start: std::time::Instant,
57) -> Result<std::process::ExitStatus, CommandError> {
58 write_stdin(&mut child, stdin_data)?;
59
60 let status = child.wait().map_err(|io_error| CommandError {
61 io_error: Some(io_error),
62 exit_status: None,
63 })?;
64
65 log::debug!("exit_status={:?} runtime={:?}", status, start.elapsed());
66
67 Ok(status)
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct EnvVariableName<'a>(Cow<'a, str>);
75
76impl EnvVariableName<'_> {
77 #[must_use]
78 pub fn as_str(&self) -> &str {
79 &self.0
80 }
81}
82
83impl AsRef<OsStr> for EnvVariableName<'_> {
84 fn as_ref(&self) -> &OsStr {
85 self.0.as_ref().as_ref()
86 }
87}
88
89impl EnvVariableName<'static> {
90 #[must_use]
96 pub const fn from_static(s: &'static str) -> Self {
97 if s.is_empty() {
98 panic!("Environment variable name cannot be empty");
99 }
100 let bytes = s.as_bytes();
101 let mut i = 0;
102 while i < bytes.len() {
103 if bytes[i] == b'=' {
104 panic!("Environment variable name cannot contain '='");
105 }
106 i += 1;
107 }
108 Self(Cow::Borrowed(s))
109 }
110}
111
112#[derive(Debug, thiserror::Error)]
113pub enum EnvVariableNameError {
114 #[error("Environment variable name cannot be empty")]
115 Empty,
116 #[error("Environment variable name cannot contain '='")]
117 ContainsEquals,
118}
119
120impl std::str::FromStr for EnvVariableName<'static> {
121 type Err = EnvVariableNameError;
122
123 fn from_str(s: &str) -> Result<Self, Self::Err> {
124 if s.is_empty() {
125 return Err(EnvVariableNameError::Empty);
126 }
127 if s.contains('=') {
128 return Err(EnvVariableNameError::ContainsEquals);
129 }
130 Ok(Self(Cow::Owned(s.to_string())))
131 }
132}
133
134#[derive(Debug)]
139pub struct Output {
140 pub stdout: Vec<u8>,
141 pub stderr: Vec<u8>,
142 pub status: std::process::ExitStatus,
143}
144
145impl Output {
146 #[must_use]
148 pub fn success(&self) -> bool {
149 self.status.success()
150 }
151
152 pub fn into_stdout_string(self) -> Result<String, std::string::FromUtf8Error> {
158 String::from_utf8(self.stdout)
159 }
160
161 pub fn into_stderr_string(self) -> Result<String, std::string::FromUtf8Error> {
167 String::from_utf8(self.stderr)
168 }
169}
170
171#[derive(Clone, Copy)]
173enum CaptureStream {
174 Stdout,
175 Stderr,
176}
177
178#[derive(Clone, Copy, Default)]
180pub enum Stdio {
181 Piped,
183 #[default]
185 Inherit,
186 Null,
188}
189
190impl From<Stdio> for std::process::Stdio {
191 fn from(stdio: Stdio) -> Self {
192 match stdio {
193 Stdio::Piped => std::process::Stdio::piped(),
194 Stdio::Inherit => std::process::Stdio::inherit(),
195 Stdio::Null => std::process::Stdio::null(),
196 }
197 }
198}
199
200pub struct Spawn {
204 command: Command,
205 stdin: Stdio,
206 stdout: Stdio,
207 stderr: Stdio,
208}
209
210impl Spawn {
211 fn new(command: Command) -> Self {
212 Self {
213 command,
214 stdin: Stdio::Inherit,
215 stdout: Stdio::Inherit,
216 stderr: Stdio::Inherit,
217 }
218 }
219
220 #[must_use]
222 pub fn stdin(mut self, stdio: Stdio) -> Self {
223 self.stdin = stdio;
224 self
225 }
226
227 #[must_use]
229 pub fn stdout(mut self, stdio: Stdio) -> Self {
230 self.stdout = stdio;
231 self
232 }
233
234 #[must_use]
236 pub fn stderr(mut self, stdio: Stdio) -> Self {
237 self.stderr = stdio;
238 self
239 }
240
241 pub fn run(mut self) -> Result<Child, CommandError> {
243 log::debug!("{:#?}", self.command.inner);
244
245 self.command.inner.stdin(self.stdin);
246 self.command.inner.stdout(self.stdout);
247 self.command.inner.stderr(self.stderr);
248
249 let inner = self
250 .command
251 .inner
252 .spawn()
253 .map_err(|io_error| CommandError {
254 io_error: Some(io_error),
255 exit_status: None,
256 })?;
257
258 Ok(Child { inner })
259 }
260}
261
262#[derive(Debug)]
266pub struct Child {
267 inner: std::process::Child,
268}
269
270impl Child {
271 pub fn stdin(&mut self) -> Option<&mut std::process::ChildStdin> {
273 self.inner.stdin.as_mut()
274 }
275
276 pub fn stdout(&mut self) -> Option<&mut std::process::ChildStdout> {
278 self.inner.stdout.as_mut()
279 }
280
281 pub fn stderr(&mut self) -> Option<&mut std::process::ChildStderr> {
283 self.inner.stderr.as_mut()
284 }
285
286 pub fn take_stdin(&mut self) -> Option<std::process::ChildStdin> {
288 self.inner.stdin.take()
289 }
290
291 pub fn take_stdout(&mut self) -> Option<std::process::ChildStdout> {
293 self.inner.stdout.take()
294 }
295
296 pub fn take_stderr(&mut self) -> Option<std::process::ChildStderr> {
298 self.inner.stderr.take()
299 }
300
301 pub fn wait(mut self) -> Result<std::process::ExitStatus, CommandError> {
303 self.inner.wait().map_err(|io_error| CommandError {
304 io_error: Some(io_error),
305 exit_status: None,
306 })
307 }
308
309 pub fn wait_with_output(self) -> Result<Output, CommandError> {
311 let output = self
312 .inner
313 .wait_with_output()
314 .map_err(|io_error| CommandError {
315 io_error: Some(io_error),
316 exit_status: None,
317 })?;
318
319 Ok(Output {
320 stdout: output.stdout,
321 stderr: output.stderr,
322 status: output.status,
323 })
324 }
325}
326
327pub struct Capture {
329 command: Command,
330 stream: CaptureStream,
331}
332
333impl Capture {
334 fn new(command: Command, stream: CaptureStream) -> Self {
335 Self { command, stream }
336 }
337
338 pub fn bytes(mut self) -> Result<Vec<u8>, CommandError> {
340 use std::process::Stdio;
341
342 log::debug!("{:#?}", self.command.inner);
343
344 self.command.inner.stdout(Stdio::piped());
345 self.command.inner.stderr(Stdio::piped());
346
347 if self.command.stdin_data.is_some() {
348 self.command.inner.stdin(Stdio::piped());
349 }
350
351 let start = std::time::Instant::now();
352
353 let child = self
354 .command
355 .inner
356 .spawn()
357 .map_err(|io_error| CommandError {
358 io_error: Some(io_error),
359 exit_status: None,
360 })?;
361
362 let output = run_and_wait(child, self.command.stdin_data, start)?;
363
364 if output.status.success() {
365 Ok(match self.stream {
366 CaptureStream::Stdout => output.stdout,
367 CaptureStream::Stderr => output.stderr,
368 })
369 } else {
370 Err(CommandError {
371 io_error: None,
372 exit_status: Some(output.status),
373 })
374 }
375 }
376
377 pub fn string(self) -> Result<String, CommandError> {
379 let bytes = self.bytes()?;
380 String::from_utf8(bytes).map_err(|utf8_error| CommandError {
381 io_error: Some(std::io::Error::new(
382 std::io::ErrorKind::InvalidData,
383 utf8_error,
384 )),
385 exit_status: None,
386 })
387 }
388}
389
390pub struct Command {
391 inner: std::process::Command,
392 stdin_data: Option<Vec<u8>>,
393}
394
395impl Command {
396 pub fn new(value: impl AsRef<OsStr>) -> Self {
397 Command {
398 inner: std::process::Command::new(value),
399 stdin_data: None,
400 }
401 }
402
403 #[cfg(feature = "test-utils")]
412 pub fn test_eq(&self, other: &Self) {
413 assert_eq!(format!("{:?}", self.inner), format!("{:?}", other.inner));
414 }
415
416 pub fn argument(mut self, value: impl AsRef<OsStr>) -> Self {
417 self.inner.arg(value);
418 self
419 }
420
421 pub fn optional_argument(mut self, optional: Option<impl AsRef<OsStr>>) -> Self {
422 if let Some(value) = optional {
423 self.inner.arg(value);
424 }
425 self
426 }
427
428 pub fn arguments<T: AsRef<OsStr>>(mut self, value: impl IntoIterator<Item = T>) -> Self {
429 self.inner.args(value);
430 self
431 }
432
433 pub fn working_directory(mut self, dir: impl AsRef<std::path::Path>) -> Self {
434 self.inner.current_dir(dir);
435 self
436 }
437
438 pub fn env(mut self, key: &EnvVariableName<'_>, val: impl AsRef<OsStr>) -> Self {
439 self.inner.env(key, val);
440 self
441 }
442
443 pub fn envs<I, K, V>(mut self, vars: I) -> Self
444 where
445 I: IntoIterator<Item = (K, V)>,
446 K: AsRef<OsStr>,
447 V: AsRef<OsStr>,
448 {
449 self.inner.envs(vars);
450 self
451 }
452
453 #[must_use]
455 pub fn env_remove(mut self, key: &EnvVariableName<'_>) -> Self {
456 self.inner.env_remove(key);
457 self
458 }
459
460 #[must_use]
461 pub fn stdin_bytes(mut self, data: impl Into<Vec<u8>>) -> Self {
462 self.stdin_data = Some(data.into());
463 self
464 }
465
466 #[must_use]
468 pub fn stdout(self) -> Capture {
469 Capture::new(self, CaptureStream::Stdout)
470 }
471
472 #[must_use]
474 pub fn stderr(self) -> Capture {
475 Capture::new(self, CaptureStream::Stderr)
476 }
477
478 #[must_use]
509 pub fn spawn(self) -> Spawn {
510 Spawn::new(self)
511 }
512
513 pub fn output(mut self) -> Result<Output, CommandError> {
518 use std::process::Stdio;
519
520 log::debug!("{:#?}", self.inner);
521
522 self.inner.stdout(Stdio::piped());
523 self.inner.stderr(Stdio::piped());
524
525 if self.stdin_data.is_some() {
526 self.inner.stdin(Stdio::piped());
527 }
528
529 let start = std::time::Instant::now();
530
531 let child = self.inner.spawn().map_err(|io_error| CommandError {
532 io_error: Some(io_error),
533 exit_status: None,
534 })?;
535
536 let output = run_and_wait(child, self.stdin_data, start)?;
537
538 Ok(Output {
539 stdout: output.stdout,
540 stderr: output.stderr,
541 status: output.status,
542 })
543 }
544
545 pub fn status(mut self) -> Result<(), CommandError> {
547 use std::process::Stdio;
548
549 log::debug!("{:#?}", self.inner);
550
551 if self.stdin_data.is_some() {
552 self.inner.stdin(Stdio::piped());
553 }
554
555 let start = std::time::Instant::now();
556
557 let child = self.inner.spawn().map_err(|io_error| CommandError {
558 io_error: Some(io_error),
559 exit_status: None,
560 })?;
561
562 let exit_status = run_and_wait_status(child, self.stdin_data, start)?;
563
564 if exit_status.success() {
565 Ok(())
566 } else {
567 Err(CommandError {
568 io_error: None,
569 exit_status: Some(exit_status),
570 })
571 }
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578
579 #[test]
580 fn test_stdout_bytes_success() {
581 assert_eq!(
582 Command::new("echo")
583 .argument("hello")
584 .stdout()
585 .bytes()
586 .unwrap(),
587 b"hello\n"
588 );
589 }
590
591 #[test]
592 fn test_stdout_bytes_nonzero_exit() {
593 let error = Command::new("sh")
594 .arguments(["-c", "exit 42"])
595 .stdout()
596 .bytes()
597 .unwrap_err();
598 assert_eq!(
599 error.exit_status.map(|status| status.code()),
600 Some(Some(42))
601 );
602 assert!(error.io_error.is_none());
603 }
604
605 #[test]
606 fn test_stdout_bytes_io_error() {
607 let error = Command::new("./nonexistent").stdout().bytes().unwrap_err();
608 assert!(error.io_error.is_some());
609 assert_eq!(error.io_error.unwrap().kind(), std::io::ErrorKind::NotFound);
610 assert!(error.exit_status.is_none());
611 }
612
613 #[test]
614 fn test_stdout_string_success() {
615 assert_eq!(
616 Command::new("echo")
617 .argument("hello")
618 .stdout()
619 .string()
620 .unwrap(),
621 "hello\n"
622 );
623 }
624
625 #[test]
626 fn test_stderr_bytes_success() {
627 assert_eq!(
628 Command::new("sh")
629 .arguments(["-c", "echo error >&2"])
630 .stderr()
631 .bytes()
632 .unwrap(),
633 b"error\n"
634 );
635 }
636
637 #[test]
638 fn test_stderr_string_success() {
639 assert_eq!(
640 Command::new("sh")
641 .arguments(["-c", "echo error >&2"])
642 .stderr()
643 .string()
644 .unwrap(),
645 "error\n"
646 );
647 }
648
649 #[test]
650 fn test_status_success() {
651 assert!(Command::new("true").status().is_ok());
652 }
653
654 #[test]
655 fn test_status_nonzero_exit() {
656 let error = Command::new("sh")
657 .arguments(["-c", "exit 42"])
658 .status()
659 .unwrap_err();
660 assert_eq!(
661 error.exit_status.map(|status| status.code()),
662 Some(Some(42))
663 );
664 assert!(error.io_error.is_none());
665 }
666
667 #[test]
668 fn test_status_io_error() {
669 let error = Command::new("./nonexistent").status().unwrap_err();
670 assert!(error.io_error.is_some());
671 assert_eq!(error.io_error.unwrap().kind(), std::io::ErrorKind::NotFound);
672 assert!(error.exit_status.is_none());
673 }
674
675 #[test]
676 fn test_env_variable_name_from_static() {
677 const NAME: EnvVariableName<'static> = EnvVariableName::from_static("PATH");
678 assert_eq!(NAME.as_str(), "PATH");
679 }
680
681 #[test]
682 fn test_env_variable_name_parse() {
683 let name: EnvVariableName = "HOME".parse().unwrap();
684 assert_eq!(name.as_str(), "HOME");
685 }
686
687 #[test]
688 fn test_env_variable_name_empty() {
689 let result: Result<EnvVariableName, _> = "".parse();
690 assert!(matches!(result, Err(EnvVariableNameError::Empty)));
691 }
692
693 #[test]
694 fn test_env_variable_name_contains_equals() {
695 let result: Result<EnvVariableName, _> = "FOO=BAR".parse();
696 assert!(matches!(result, Err(EnvVariableNameError::ContainsEquals)));
697 }
698
699 #[test]
700 fn test_env_with_variable() {
701 let name: EnvVariableName = "MY_VAR".parse().unwrap();
702 let output = Command::new("sh")
703 .arguments(["-c", "echo $MY_VAR"])
704 .env(&name, "hello")
705 .stdout()
706 .string()
707 .unwrap();
708 assert_eq!(output, "hello\n");
709 }
710
711 #[test]
712 fn test_stdin_bytes() {
713 let output = Command::new("cat")
714 .stdin_bytes(b"hello world".as_slice())
715 .stdout()
716 .string()
717 .unwrap();
718 assert_eq!(output, "hello world");
719 }
720
721 #[test]
722 fn test_stdin_bytes_vec() {
723 let output = Command::new("cat")
724 .stdin_bytes(vec![104, 105])
725 .stdout()
726 .string()
727 .unwrap();
728 assert_eq!(output, "hi");
729 }
730
731 #[test]
732 fn test_output_success() {
733 let output = Command::new("echo").argument("hello").output().unwrap();
734 assert!(output.success());
735 assert_eq!(output.stdout, b"hello\n");
736 assert!(output.stderr.is_empty());
737 }
738
739 #[test]
740 fn test_output_failure_with_stderr() {
741 let output = Command::new("sh")
742 .arguments(["-c", "echo error >&2; exit 1"])
743 .output()
744 .unwrap();
745 assert!(!output.success());
746 assert_eq!(output.into_stderr_string().unwrap(), "error\n");
747 }
748
749 #[test]
750 fn test_output_io_error() {
751 let error = Command::new("./nonexistent").output().unwrap_err();
752 assert!(error.io_error.is_some());
753 assert_eq!(error.io_error.unwrap().kind(), std::io::ErrorKind::NotFound);
754 }
755
756 #[test]
757 fn test_spawn_with_piped_stdout() {
758 use std::io::Read;
759
760 let mut child = Command::new("echo")
761 .argument("hello")
762 .spawn()
763 .stdout(Stdio::Piped)
764 .run()
765 .unwrap();
766
767 let mut output = String::new();
768 child.stdout().unwrap().read_to_string(&mut output).unwrap();
769 assert_eq!(output, "hello\n");
770
771 let status = child.wait().unwrap();
772 assert!(status.success());
773 }
774
775 #[test]
776 fn test_spawn_with_piped_stdin() {
777 use std::io::Write;
778
779 let mut child = Command::new("cat")
780 .spawn()
781 .stdin(Stdio::Piped)
782 .stdout(Stdio::Piped)
783 .run()
784 .unwrap();
785
786 child.stdin().unwrap().write_all(b"hello").unwrap();
787 drop(child.take_stdin());
788
789 let output = child.wait_with_output().unwrap();
790 assert!(output.success());
791 assert_eq!(output.stdout, b"hello");
792 }
793
794 #[test]
795 fn test_spawn_wait() {
796 let child = Command::new("true").spawn().run().unwrap();
797
798 let status = child.wait().unwrap();
799 assert!(status.success());
800 }
801
802 #[test]
803 fn test_spawn_wait_with_output() {
804 let child = Command::new("sh")
805 .arguments(["-c", "echo out; echo err >&2"])
806 .spawn()
807 .stdout(Stdio::Piped)
808 .stderr(Stdio::Piped)
809 .run()
810 .unwrap();
811
812 let output = child.wait_with_output().unwrap();
813 assert!(output.success());
814 assert_eq!(output.stdout, b"out\n");
815 assert_eq!(output.stderr, b"err\n");
816 }
817
818 #[test]
819 fn test_spawn_io_error() {
820 let error = Command::new("./nonexistent").spawn().run().unwrap_err();
821 assert!(error.io_error.is_some());
822 assert_eq!(error.io_error.unwrap().kind(), std::io::ErrorKind::NotFound);
823 }
824
825 #[test]
826 fn test_spawn_take_handles() {
827 use std::io::{Read, Write};
828
829 let mut child = Command::new("cat")
830 .spawn()
831 .stdin(Stdio::Piped)
832 .stdout(Stdio::Piped)
833 .run()
834 .unwrap();
835
836 let mut stdin = child.take_stdin().unwrap();
837 stdin.write_all(b"test").unwrap();
838 drop(stdin);
839
840 let mut stdout = child.take_stdout().unwrap();
841 let mut output = String::new();
842 stdout.read_to_string(&mut output).unwrap();
843 assert_eq!(output, "test");
844
845 child.wait().unwrap();
846 }
847}