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, PartialOrd, Ord)]
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 std::fmt::Display for EnvVariableName<'_> {
90 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 formatter.write_str(self.as_str())
92 }
93}
94
95impl EnvVariableName<'static> {
96 #[must_use]
103 pub const fn from_static_or_panic(s: &'static str) -> Self {
104 match validate_env_variable_name(s) {
105 Ok(()) => {}
106 Err(EnvVariableNameError::Empty) => {
107 panic!("Environment variable name cannot be empty");
108 }
109 Err(EnvVariableNameError::ContainsEquals) => {
110 panic!("Environment variable name cannot contain '='");
111 }
112 }
113 Self(Cow::Borrowed(s))
114 }
115}
116
117#[derive(Debug, thiserror::Error)]
118pub enum EnvVariableNameError {
119 #[error("Environment variable name cannot be empty")]
120 Empty,
121 #[error("Environment variable name cannot contain '='")]
122 ContainsEquals,
123}
124
125impl std::str::FromStr for EnvVariableName<'static> {
126 type Err = EnvVariableNameError;
127
128 fn from_str(s: &str) -> Result<Self, Self::Err> {
129 validate_env_variable_name(s).map(|()| Self(Cow::Owned(s.to_string())))
130 }
131}
132
133const fn validate_env_variable_name(s: &str) -> Result<(), EnvVariableNameError> {
134 if s.is_empty() {
135 return Err(EnvVariableNameError::Empty);
136 }
137 let bytes = s.as_bytes();
138 let mut i = 0;
139 while i < bytes.len() {
141 if bytes[i] == b'=' {
142 return Err(EnvVariableNameError::ContainsEquals);
143 }
144 i += 1;
145 }
146 Ok(())
147}
148
149#[derive(Debug)]
154pub struct Output {
155 pub stdout: Vec<u8>,
156 pub stderr: Vec<u8>,
157 pub status: std::process::ExitStatus,
158}
159
160impl Output {
161 #[must_use]
163 pub fn success(&self) -> bool {
164 self.status.success()
165 }
166
167 pub fn into_stdout_string(self) -> Result<String, std::string::FromUtf8Error> {
173 String::from_utf8(self.stdout)
174 }
175
176 pub fn into_stderr_string(self) -> Result<String, std::string::FromUtf8Error> {
182 String::from_utf8(self.stderr)
183 }
184}
185
186#[derive(Clone, Copy)]
188enum CaptureStream {
189 Stdout,
190 Stderr,
191}
192
193#[derive(Clone, Copy, Default)]
195pub enum Stdio {
196 Piped,
198 #[default]
200 Inherit,
201 Null,
203}
204
205impl From<Stdio> for std::process::Stdio {
206 fn from(stdio: Stdio) -> Self {
207 match stdio {
208 Stdio::Piped => std::process::Stdio::piped(),
209 Stdio::Inherit => std::process::Stdio::inherit(),
210 Stdio::Null => std::process::Stdio::null(),
211 }
212 }
213}
214
215pub struct Spawn {
219 command: Command,
220 stdin: Stdio,
221 stdout: Stdio,
222 stderr: Stdio,
223}
224
225impl Spawn {
226 fn new(command: Command) -> Self {
227 Self {
228 command,
229 stdin: Stdio::Inherit,
230 stdout: Stdio::Inherit,
231 stderr: Stdio::Inherit,
232 }
233 }
234
235 #[must_use]
237 pub fn stdin(mut self, stdio: Stdio) -> Self {
238 self.stdin = stdio;
239 self
240 }
241
242 #[must_use]
244 pub fn stdout(mut self, stdio: Stdio) -> Self {
245 self.stdout = stdio;
246 self
247 }
248
249 #[must_use]
251 pub fn stderr(mut self, stdio: Stdio) -> Self {
252 self.stderr = stdio;
253 self
254 }
255
256 pub fn run(mut self) -> Result<Child, CommandError> {
258 log::debug!("{:#?}", self.command.inner);
259
260 self.command.inner.stdin(self.stdin);
261 self.command.inner.stdout(self.stdout);
262 self.command.inner.stderr(self.stderr);
263
264 let inner = self
265 .command
266 .inner
267 .spawn()
268 .map_err(|io_error| CommandError {
269 io_error: Some(io_error),
270 exit_status: None,
271 })?;
272
273 Ok(Child { inner })
274 }
275}
276
277#[derive(Debug)]
281pub struct Child {
282 inner: std::process::Child,
283}
284
285impl Child {
286 pub fn stdin(&mut self) -> Option<&mut std::process::ChildStdin> {
288 self.inner.stdin.as_mut()
289 }
290
291 pub fn stdout(&mut self) -> Option<&mut std::process::ChildStdout> {
293 self.inner.stdout.as_mut()
294 }
295
296 pub fn stderr(&mut self) -> Option<&mut std::process::ChildStderr> {
298 self.inner.stderr.as_mut()
299 }
300
301 pub fn take_stdin(&mut self) -> Option<std::process::ChildStdin> {
303 self.inner.stdin.take()
304 }
305
306 pub fn take_stdout(&mut self) -> Option<std::process::ChildStdout> {
308 self.inner.stdout.take()
309 }
310
311 pub fn take_stderr(&mut self) -> Option<std::process::ChildStderr> {
313 self.inner.stderr.take()
314 }
315
316 pub fn wait(mut self) -> Result<std::process::ExitStatus, CommandError> {
318 self.inner.wait().map_err(|io_error| CommandError {
319 io_error: Some(io_error),
320 exit_status: None,
321 })
322 }
323
324 pub fn wait_with_output(self) -> Result<Output, CommandError> {
326 let output = self
327 .inner
328 .wait_with_output()
329 .map_err(|io_error| CommandError {
330 io_error: Some(io_error),
331 exit_status: None,
332 })?;
333
334 Ok(Output {
335 stdout: output.stdout,
336 stderr: output.stderr,
337 status: output.status,
338 })
339 }
340}
341
342pub struct Capture {
344 command: Command,
345 stream: CaptureStream,
346}
347
348impl Capture {
349 fn new(command: Command, stream: CaptureStream) -> Self {
350 Self { command, stream }
351 }
352
353 pub fn bytes(mut self) -> Result<Vec<u8>, CommandError> {
355 use std::process::Stdio;
356
357 log::debug!("{:#?}", self.command.inner);
358
359 self.command.inner.stdout(Stdio::piped());
360 self.command.inner.stderr(Stdio::piped());
361
362 if self.command.stdin_data.is_some() {
363 self.command.inner.stdin(Stdio::piped());
364 }
365
366 let start = std::time::Instant::now();
367
368 let child = self
369 .command
370 .inner
371 .spawn()
372 .map_err(|io_error| CommandError {
373 io_error: Some(io_error),
374 exit_status: None,
375 })?;
376
377 let output = run_and_wait(child, self.command.stdin_data, start)?;
378
379 if output.status.success() {
380 Ok(match self.stream {
381 CaptureStream::Stdout => output.stdout,
382 CaptureStream::Stderr => output.stderr,
383 })
384 } else {
385 Err(CommandError {
386 io_error: None,
387 exit_status: Some(output.status),
388 })
389 }
390 }
391
392 pub fn string(self) -> Result<String, CommandError> {
394 let bytes = self.bytes()?;
395 String::from_utf8(bytes).map_err(|utf8_error| CommandError {
396 io_error: Some(std::io::Error::new(
397 std::io::ErrorKind::InvalidData,
398 utf8_error,
399 )),
400 exit_status: None,
401 })
402 }
403}
404
405pub struct Command {
406 inner: std::process::Command,
407 stdin_data: Option<Vec<u8>>,
408}
409
410impl Command {
411 pub fn new(value: impl AsRef<OsStr>) -> Self {
412 Command {
413 inner: std::process::Command::new(value),
414 stdin_data: None,
415 }
416 }
417
418 #[cfg(feature = "test-utils")]
427 pub fn test_eq(&self, other: &Self) {
428 assert_eq!(format!("{:?}", self.inner), format!("{:?}", other.inner));
429 }
430
431 pub fn argument(mut self, value: impl AsRef<OsStr>) -> Self {
432 self.inner.arg(value);
433 self
434 }
435
436 pub fn optional_argument(mut self, optional: Option<impl AsRef<OsStr>>) -> Self {
437 if let Some(value) = optional {
438 self.inner.arg(value);
439 }
440 self
441 }
442
443 pub fn option(mut self, name: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> Self {
450 self.inner.arg(name);
451 self.inner.arg(value);
452 self
453 }
454
455 pub fn optional_option(
462 mut self,
463 name: impl AsRef<OsStr>,
464 value: Option<impl AsRef<OsStr>>,
465 ) -> Self {
466 if let Some(value) = value {
467 self.inner.arg(name);
468 self.inner.arg(value);
469 }
470 self
471 }
472
473 pub fn arguments<T: AsRef<OsStr>>(mut self, value: impl IntoIterator<Item = T>) -> Self {
474 self.inner.args(value);
475 self
476 }
477
478 pub fn working_directory(mut self, dir: impl AsRef<std::path::Path>) -> Self {
479 self.inner.current_dir(dir);
480 self
481 }
482
483 pub fn env(mut self, key: &EnvVariableName<'_>, val: impl AsRef<OsStr>) -> Self {
484 self.inner.env(key, val);
485 self
486 }
487
488 pub fn envs<'a, I, V>(mut self, vars: I) -> Self
489 where
490 I: IntoIterator<Item = (EnvVariableName<'a>, V)>,
491 V: AsRef<OsStr>,
492 {
493 for (key, val) in vars {
494 self.inner.env(key, val);
495 }
496 self
497 }
498
499 #[must_use]
501 pub fn env_remove(mut self, key: &EnvVariableName<'_>) -> Self {
502 self.inner.env_remove(key);
503 self
504 }
505
506 #[must_use]
507 pub fn stdin_bytes(mut self, data: impl Into<Vec<u8>>) -> Self {
508 self.stdin_data = Some(data.into());
509 self
510 }
511
512 #[must_use]
514 pub fn stdout(self) -> Capture {
515 Capture::new(self, CaptureStream::Stdout)
516 }
517
518 #[must_use]
520 pub fn stderr(self) -> Capture {
521 Capture::new(self, CaptureStream::Stderr)
522 }
523
524 #[must_use]
555 pub fn spawn(self) -> Spawn {
556 Spawn::new(self)
557 }
558
559 pub fn output(mut self) -> Result<Output, CommandError> {
564 use std::process::Stdio;
565
566 log::debug!("{:#?}", self.inner);
567
568 self.inner.stdout(Stdio::piped());
569 self.inner.stderr(Stdio::piped());
570
571 if self.stdin_data.is_some() {
572 self.inner.stdin(Stdio::piped());
573 }
574
575 let start = std::time::Instant::now();
576
577 let child = self.inner.spawn().map_err(|io_error| CommandError {
578 io_error: Some(io_error),
579 exit_status: None,
580 })?;
581
582 let output = run_and_wait(child, self.stdin_data, start)?;
583
584 Ok(Output {
585 stdout: output.stdout,
586 stderr: output.stderr,
587 status: output.status,
588 })
589 }
590
591 pub fn status(mut self) -> Result<(), CommandError> {
593 use std::process::Stdio;
594
595 log::debug!("{:#?}", self.inner);
596
597 if self.stdin_data.is_some() {
598 self.inner.stdin(Stdio::piped());
599 }
600
601 let start = std::time::Instant::now();
602
603 let child = self.inner.spawn().map_err(|io_error| CommandError {
604 io_error: Some(io_error),
605 exit_status: None,
606 })?;
607
608 let exit_status = run_and_wait_status(child, self.stdin_data, start)?;
609
610 if exit_status.success() {
611 Ok(())
612 } else {
613 Err(CommandError {
614 io_error: None,
615 exit_status: Some(exit_status),
616 })
617 }
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[test]
626 fn test_stdout_bytes_success() {
627 assert_eq!(
628 Command::new("echo")
629 .argument("hello")
630 .stdout()
631 .bytes()
632 .unwrap(),
633 b"hello\n"
634 );
635 }
636
637 #[test]
638 fn test_stdout_bytes_nonzero_exit() {
639 let error = Command::new("sh")
640 .arguments(["-c", "exit 42"])
641 .stdout()
642 .bytes()
643 .unwrap_err();
644 assert_eq!(
645 error.exit_status.map(|status| status.code()),
646 Some(Some(42))
647 );
648 assert!(error.io_error.is_none());
649 }
650
651 #[test]
652 fn test_stdout_bytes_io_error() {
653 let error = Command::new("./nonexistent").stdout().bytes().unwrap_err();
654 assert!(error.io_error.is_some());
655 assert_eq!(error.io_error.unwrap().kind(), std::io::ErrorKind::NotFound);
656 assert!(error.exit_status.is_none());
657 }
658
659 #[test]
660 fn test_stdout_string_success() {
661 assert_eq!(
662 Command::new("echo")
663 .argument("hello")
664 .stdout()
665 .string()
666 .unwrap(),
667 "hello\n"
668 );
669 }
670
671 #[test]
672 fn test_stderr_bytes_success() {
673 assert_eq!(
674 Command::new("sh")
675 .arguments(["-c", "echo error >&2"])
676 .stderr()
677 .bytes()
678 .unwrap(),
679 b"error\n"
680 );
681 }
682
683 #[test]
684 fn test_stderr_string_success() {
685 assert_eq!(
686 Command::new("sh")
687 .arguments(["-c", "echo error >&2"])
688 .stderr()
689 .string()
690 .unwrap(),
691 "error\n"
692 );
693 }
694
695 #[test]
696 fn test_status_success() {
697 assert!(Command::new("true").status().is_ok());
698 }
699
700 #[test]
701 fn test_status_nonzero_exit() {
702 let error = Command::new("sh")
703 .arguments(["-c", "exit 42"])
704 .status()
705 .unwrap_err();
706 assert_eq!(
707 error.exit_status.map(|status| status.code()),
708 Some(Some(42))
709 );
710 assert!(error.io_error.is_none());
711 }
712
713 #[test]
714 fn test_status_io_error() {
715 let error = Command::new("./nonexistent").status().unwrap_err();
716 assert!(error.io_error.is_some());
717 assert_eq!(error.io_error.unwrap().kind(), std::io::ErrorKind::NotFound);
718 assert!(error.exit_status.is_none());
719 }
720
721 #[test]
722 fn test_env_variable_name_from_static_or_panic() {
723 const NAME: EnvVariableName<'static> = EnvVariableName::from_static_or_panic("PATH");
724 assert_eq!(NAME.as_str(), "PATH");
725 }
726
727 #[test]
728 fn test_env_variable_name_parse() {
729 let name: EnvVariableName = "HOME".parse().unwrap();
730 assert_eq!(name.as_str(), "HOME");
731 }
732
733 #[test]
734 fn test_env_variable_name_empty() {
735 let result: Result<EnvVariableName, _> = "".parse();
736 assert!(matches!(result, Err(EnvVariableNameError::Empty)));
737 }
738
739 #[test]
740 fn test_env_variable_name_contains_equals() {
741 let result: Result<EnvVariableName, _> = "FOO=BAR".parse();
742 assert!(matches!(result, Err(EnvVariableNameError::ContainsEquals)));
743 }
744
745 #[test]
746 fn test_env_with_variable() {
747 let name: EnvVariableName = "MY_VAR".parse().unwrap();
748 let output = Command::new("sh")
749 .arguments(["-c", "echo $MY_VAR"])
750 .env(&name, "hello")
751 .stdout()
752 .string()
753 .unwrap();
754 assert_eq!(output, "hello\n");
755 }
756
757 #[test]
758 fn test_stdin_bytes() {
759 let output = Command::new("cat")
760 .stdin_bytes(b"hello world".as_slice())
761 .stdout()
762 .string()
763 .unwrap();
764 assert_eq!(output, "hello world");
765 }
766
767 #[test]
768 fn test_stdin_bytes_vec() {
769 let output = Command::new("cat")
770 .stdin_bytes(vec![104, 105])
771 .stdout()
772 .string()
773 .unwrap();
774 assert_eq!(output, "hi");
775 }
776
777 #[test]
778 fn test_output_success() {
779 let output = Command::new("echo").argument("hello").output().unwrap();
780 assert!(output.success());
781 assert_eq!(output.stdout, b"hello\n");
782 assert!(output.stderr.is_empty());
783 }
784
785 #[test]
786 fn test_output_failure_with_stderr() {
787 let output = Command::new("sh")
788 .arguments(["-c", "echo error >&2; exit 1"])
789 .output()
790 .unwrap();
791 assert!(!output.success());
792 assert_eq!(output.into_stderr_string().unwrap(), "error\n");
793 }
794
795 #[test]
796 fn test_output_io_error() {
797 let error = Command::new("./nonexistent").output().unwrap_err();
798 assert!(error.io_error.is_some());
799 assert_eq!(error.io_error.unwrap().kind(), std::io::ErrorKind::NotFound);
800 }
801
802 #[test]
803 fn test_spawn_with_piped_stdout() {
804 use std::io::Read;
805
806 let mut child = Command::new("echo")
807 .argument("hello")
808 .spawn()
809 .stdout(Stdio::Piped)
810 .run()
811 .unwrap();
812
813 let mut output = String::new();
814 child.stdout().unwrap().read_to_string(&mut output).unwrap();
815 assert_eq!(output, "hello\n");
816
817 let status = child.wait().unwrap();
818 assert!(status.success());
819 }
820
821 #[test]
822 fn test_spawn_with_piped_stdin() {
823 use std::io::Write;
824
825 let mut child = Command::new("cat")
826 .spawn()
827 .stdin(Stdio::Piped)
828 .stdout(Stdio::Piped)
829 .run()
830 .unwrap();
831
832 child.stdin().unwrap().write_all(b"hello").unwrap();
833 drop(child.take_stdin());
834
835 let output = child.wait_with_output().unwrap();
836 assert!(output.success());
837 assert_eq!(output.stdout, b"hello");
838 }
839
840 #[test]
841 fn test_spawn_wait() {
842 let child = Command::new("true").spawn().run().unwrap();
843
844 let status = child.wait().unwrap();
845 assert!(status.success());
846 }
847
848 #[test]
849 fn test_spawn_wait_with_output() {
850 let child = Command::new("sh")
851 .arguments(["-c", "echo out; echo err >&2"])
852 .spawn()
853 .stdout(Stdio::Piped)
854 .stderr(Stdio::Piped)
855 .run()
856 .unwrap();
857
858 let output = child.wait_with_output().unwrap();
859 assert!(output.success());
860 assert_eq!(output.stdout, b"out\n");
861 assert_eq!(output.stderr, b"err\n");
862 }
863
864 #[test]
865 fn test_spawn_io_error() {
866 let error = Command::new("./nonexistent").spawn().run().unwrap_err();
867 assert!(error.io_error.is_some());
868 assert_eq!(error.io_error.unwrap().kind(), std::io::ErrorKind::NotFound);
869 }
870
871 #[test]
872 fn test_spawn_take_handles() {
873 use std::io::{Read, Write};
874
875 let mut child = Command::new("cat")
876 .spawn()
877 .stdin(Stdio::Piped)
878 .stdout(Stdio::Piped)
879 .run()
880 .unwrap();
881
882 let mut stdin = child.take_stdin().unwrap();
883 stdin.write_all(b"test").unwrap();
884 drop(stdin);
885
886 let mut stdout = child.take_stdout().unwrap();
887 let mut output = String::new();
888 stdout.read_to_string(&mut output).unwrap();
889 assert_eq!(output, "test");
890
891 child.wait().unwrap();
892 }
893
894 #[test]
895 fn test_option() {
896 let output = Command::new("echo")
897 .option("-n", "hello")
898 .stdout()
899 .string()
900 .unwrap();
901 assert_eq!(output, "hello");
902 }
903
904 #[test]
905 fn test_optional_option_some() {
906 let output = Command::new("echo")
907 .optional_option("-n", Some("hello"))
908 .stdout()
909 .string()
910 .unwrap();
911 assert_eq!(output, "hello");
912 }
913
914 #[test]
915 fn test_optional_option_none() {
916 let output = Command::new("echo")
917 .optional_option("-n", None::<&str>)
918 .argument("hello")
919 .stdout()
920 .string()
921 .unwrap();
922 assert_eq!(output, "hello\n");
923 }
924}