1#![doc = include_str!("../README.md")]
2
3use std::borrow::Cow;
4use std::ffi::OsStr;
5use std::marker::PhantomData;
6
7#[derive(Debug, thiserror::Error)]
8pub enum CommandError {
9 #[error("command IO failure")]
10 Io(#[source] std::io::Error),
11 #[error("command exited with {0}")]
12 ExitStatus(std::process::ExitStatus),
13}
14
15async fn write_stdin(
16 child: &mut tokio::process::Child,
17 stdin_data: Option<Vec<u8>>,
18) -> Result<(), CommandError> {
19 use tokio::io::AsyncWriteExt;
20
21 if let Some(data) = stdin_data {
22 child
23 .stdin
24 .take()
25 .unwrap()
26 .write_all(&data)
27 .await
28 .map_err(CommandError::Io)?;
29 }
30
31 Ok(())
32}
33
34async fn run_and_wait(
35 mut child: tokio::process::Child,
36 stdin_data: Option<Vec<u8>>,
37 start: std::time::Instant,
38) -> Result<std::process::Output, CommandError> {
39 write_stdin(&mut child, stdin_data).await?;
40
41 let output = child.wait_with_output().await.map_err(CommandError::Io)?;
42
43 log::debug!(
44 "exit_status={:?} runtime={:?}",
45 output.status,
46 start.elapsed()
47 );
48
49 Ok(output)
50}
51
52async fn run_and_wait_status(
53 mut child: tokio::process::Child,
54 stdin_data: Option<Vec<u8>>,
55 start: std::time::Instant,
56) -> Result<std::process::ExitStatus, CommandError> {
57 write_stdin(&mut child, stdin_data).await?;
58
59 let status = child.wait().await.map_err(CommandError::Io)?;
60
61 log::debug!("exit_status={:?} runtime={:?}", status, start.elapsed());
62
63 Ok(status)
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
70pub struct EnvVariableName<'a>(Cow<'a, str>);
71
72impl EnvVariableName<'_> {
73 #[must_use]
74 pub fn as_str(&self) -> &str {
75 &self.0
76 }
77}
78
79impl AsRef<OsStr> for EnvVariableName<'_> {
80 fn as_ref(&self) -> &OsStr {
81 self.0.as_ref().as_ref()
82 }
83}
84
85impl std::fmt::Display for EnvVariableName<'_> {
86 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 formatter.write_str(self.as_str())
88 }
89}
90
91impl EnvVariableName<'static> {
92 #[must_use]
99 pub const fn from_static_or_panic(s: &'static str) -> Self {
100 match validate_env_variable_name(s) {
101 Ok(()) => {}
102 Err(EnvVariableNameError::Empty) => {
103 panic!("Environment variable name cannot be empty");
104 }
105 Err(EnvVariableNameError::ContainsEquals) => {
106 panic!("Environment variable name cannot contain '='");
107 }
108 }
109 Self(Cow::Borrowed(s))
110 }
111}
112
113#[derive(Debug, thiserror::Error)]
114pub enum EnvVariableNameError {
115 #[error("Environment variable name cannot be empty")]
116 Empty,
117 #[error("Environment variable name cannot contain '='")]
118 ContainsEquals,
119}
120
121impl std::str::FromStr for EnvVariableName<'static> {
122 type Err = EnvVariableNameError;
123
124 fn from_str(s: &str) -> Result<Self, Self::Err> {
125 validate_env_variable_name(s).map(|()| Self(Cow::Owned(s.to_string())))
126 }
127}
128
129const fn validate_env_variable_name(s: &str) -> Result<(), EnvVariableNameError> {
130 if s.is_empty() {
131 return Err(EnvVariableNameError::Empty);
132 }
133 let bytes = s.as_bytes();
134 let mut i = 0;
135 while i < bytes.len() {
137 if bytes[i] == b'=' {
138 return Err(EnvVariableNameError::ContainsEquals);
139 }
140 i += 1;
141 }
142 Ok(())
143}
144
145mod sealed {
146 pub trait Sealed {}
147}
148
149pub trait StreamMarker: sealed::Sealed {}
151
152pub struct Stdout;
154
155pub struct Stderr;
157
158impl sealed::Sealed for Stdout {}
159impl sealed::Sealed for Stderr {}
160impl StreamMarker for Stdout {}
161impl StreamMarker for Stderr {}
162
163#[derive(Debug)]
165pub struct CaptureSingleResult {
166 pub bytes: Vec<u8>,
167 pub status: std::process::ExitStatus,
168}
169
170#[derive(Debug)]
172pub struct CaptureAllResult {
173 pub stdout: Vec<u8>,
174 pub stderr: Vec<u8>,
175 pub status: std::process::ExitStatus,
176}
177
178async fn run_capture(
179 mut command: Command,
180 accept_nonzero_exit: bool,
181) -> Result<std::process::Output, CommandError> {
182 log::debug!("{:#?}", command.inner);
183
184 if command.stdin_data.is_some() {
185 command.inner.stdin(std::process::Stdio::piped());
186 }
187
188 let start = std::time::Instant::now();
189
190 let child = command.inner.spawn().map_err(CommandError::Io)?;
191
192 let output = run_and_wait(child, command.stdin_data, start).await?;
193
194 if accept_nonzero_exit || output.status.success() {
195 Ok(output)
196 } else {
197 Err(CommandError::ExitStatus(output.status))
198 }
199}
200
201pub struct CaptureSingle<S: StreamMarker> {
206 inner: tokio::process::Command,
207 stdin_data: Option<Vec<u8>>,
208 accept_nonzero_exit: bool,
209 _marker: PhantomData<S>,
210}
211
212impl<S: StreamMarker> CaptureSingle<S> {
213 #[must_use]
215 pub fn accept_nonzero_exit(mut self) -> Self {
216 self.accept_nonzero_exit = true;
217 self
218 }
219}
220
221impl CaptureSingle<Stdout> {
222 #[must_use]
224 pub fn stderr_capture(mut self) -> CaptureAll {
225 self.inner.stdout(std::process::Stdio::piped());
226 self.inner.stderr(std::process::Stdio::piped());
227 CaptureAll {
228 inner: self.inner,
229 stdin_data: self.stdin_data,
230 accept_nonzero_exit: self.accept_nonzero_exit,
231 }
232 }
233
234 #[must_use]
236 pub fn stderr_null(mut self) -> Self {
237 self.inner.stderr(std::process::Stdio::null());
238 self
239 }
240
241 #[must_use]
243 pub fn stderr_inherit(mut self) -> Self {
244 self.inner.stderr(std::process::Stdio::inherit());
245 self
246 }
247
248 #[must_use]
250 pub fn stdout_null(mut self) -> Command {
251 self.inner.stdout(std::process::Stdio::null());
252 Command {
253 inner: self.inner,
254 stdin_data: self.stdin_data,
255 }
256 }
257
258 #[must_use]
260 pub fn stdout_inherit(mut self) -> Command {
261 self.inner.stdout(std::process::Stdio::inherit());
262 Command {
263 inner: self.inner,
264 stdin_data: self.stdin_data,
265 }
266 }
267
268 pub async fn run(mut self) -> Result<CaptureSingleResult, CommandError> {
270 self.inner.stdout(std::process::Stdio::piped());
271
272 let command = Command {
273 inner: self.inner,
274 stdin_data: self.stdin_data,
275 };
276
277 let output = run_capture(command, self.accept_nonzero_exit).await?;
278
279 Ok(CaptureSingleResult {
280 bytes: output.stdout,
281 status: output.status,
282 })
283 }
284
285 pub async fn bytes(self) -> Result<Vec<u8>, CommandError> {
287 Ok(self.run().await?.bytes)
288 }
289
290 pub async fn string(self) -> Result<String, CommandError> {
292 let bytes = self.bytes().await?;
293 String::from_utf8(bytes).map_err(|utf8_error| {
294 CommandError::Io(std::io::Error::new(
295 std::io::ErrorKind::InvalidData,
296 utf8_error,
297 ))
298 })
299 }
300}
301
302impl CaptureSingle<Stderr> {
303 #[must_use]
305 pub fn stdout_capture(mut self) -> CaptureAll {
306 self.inner.stdout(std::process::Stdio::piped());
307 self.inner.stderr(std::process::Stdio::piped());
308 CaptureAll {
309 inner: self.inner,
310 stdin_data: self.stdin_data,
311 accept_nonzero_exit: self.accept_nonzero_exit,
312 }
313 }
314
315 #[must_use]
317 pub fn stdout_null(mut self) -> Self {
318 self.inner.stdout(std::process::Stdio::null());
319 self
320 }
321
322 #[must_use]
324 pub fn stdout_inherit(mut self) -> Self {
325 self.inner.stdout(std::process::Stdio::inherit());
326 self
327 }
328
329 #[must_use]
331 pub fn stderr_null(mut self) -> Command {
332 self.inner.stderr(std::process::Stdio::null());
333 Command {
334 inner: self.inner,
335 stdin_data: self.stdin_data,
336 }
337 }
338
339 #[must_use]
341 pub fn stderr_inherit(mut self) -> Command {
342 self.inner.stderr(std::process::Stdio::inherit());
343 Command {
344 inner: self.inner,
345 stdin_data: self.stdin_data,
346 }
347 }
348
349 pub async fn run(mut self) -> Result<CaptureSingleResult, CommandError> {
351 self.inner.stderr(std::process::Stdio::piped());
352
353 let command = Command {
354 inner: self.inner,
355 stdin_data: self.stdin_data,
356 };
357
358 let output = run_capture(command, self.accept_nonzero_exit).await?;
359
360 Ok(CaptureSingleResult {
361 bytes: output.stderr,
362 status: output.status,
363 })
364 }
365
366 pub async fn bytes(self) -> Result<Vec<u8>, CommandError> {
368 Ok(self.run().await?.bytes)
369 }
370
371 pub async fn string(self) -> Result<String, CommandError> {
373 let bytes = self.bytes().await?;
374 String::from_utf8(bytes).map_err(|utf8_error| {
375 CommandError::Io(std::io::Error::new(
376 std::io::ErrorKind::InvalidData,
377 utf8_error,
378 ))
379 })
380 }
381}
382
383pub struct CaptureAll {
385 inner: tokio::process::Command,
386 stdin_data: Option<Vec<u8>>,
387 accept_nonzero_exit: bool,
388}
389
390impl CaptureAll {
391 #[must_use]
393 pub fn accept_nonzero_exit(mut self) -> Self {
394 self.accept_nonzero_exit = true;
395 self
396 }
397
398 #[must_use]
400 pub fn stdout_null(mut self) -> CaptureSingle<Stderr> {
401 self.inner.stdout(std::process::Stdio::null());
402 CaptureSingle {
403 inner: self.inner,
404 stdin_data: self.stdin_data,
405 accept_nonzero_exit: self.accept_nonzero_exit,
406 _marker: PhantomData,
407 }
408 }
409
410 #[must_use]
412 pub fn stdout_inherit(mut self) -> CaptureSingle<Stderr> {
413 self.inner.stdout(std::process::Stdio::inherit());
414 CaptureSingle {
415 inner: self.inner,
416 stdin_data: self.stdin_data,
417 accept_nonzero_exit: self.accept_nonzero_exit,
418 _marker: PhantomData,
419 }
420 }
421
422 #[must_use]
424 pub fn stderr_null(mut self) -> CaptureSingle<Stdout> {
425 self.inner.stderr(std::process::Stdio::null());
426 CaptureSingle {
427 inner: self.inner,
428 stdin_data: self.stdin_data,
429 accept_nonzero_exit: self.accept_nonzero_exit,
430 _marker: PhantomData,
431 }
432 }
433
434 #[must_use]
436 pub fn stderr_inherit(mut self) -> CaptureSingle<Stdout> {
437 self.inner.stderr(std::process::Stdio::inherit());
438 CaptureSingle {
439 inner: self.inner,
440 stdin_data: self.stdin_data,
441 accept_nonzero_exit: self.accept_nonzero_exit,
442 _marker: PhantomData,
443 }
444 }
445
446 pub async fn run(mut self) -> Result<CaptureAllResult, CommandError> {
448 self.inner.stdout(std::process::Stdio::piped());
449 self.inner.stderr(std::process::Stdio::piped());
450
451 let command = Command {
452 inner: self.inner,
453 stdin_data: self.stdin_data,
454 };
455
456 let output = run_capture(command, self.accept_nonzero_exit).await?;
457
458 Ok(CaptureAllResult {
459 stdout: output.stdout,
460 stderr: output.stderr,
461 status: output.status,
462 })
463 }
464}
465
466pub struct Command {
467 inner: tokio::process::Command,
468 stdin_data: Option<Vec<u8>>,
469}
470
471impl Command {
472 pub fn new(value: impl AsRef<OsStr>) -> Self {
473 Command {
474 inner: tokio::process::Command::new(value),
475 stdin_data: None,
476 }
477 }
478
479 #[cfg(feature = "test-utils")]
488 pub fn test_eq(&self, other: &Self) {
489 assert_eq!(format!("{:?}", self.inner), format!("{:?}", other.inner));
490 }
491
492 pub fn argument(mut self, value: impl AsRef<OsStr>) -> Self {
493 self.inner.arg(value);
494 self
495 }
496
497 pub fn optional_argument(mut self, optional: Option<impl AsRef<OsStr>>) -> Self {
498 if let Some(value) = optional {
499 self.inner.arg(value);
500 }
501 self
502 }
503
504 pub fn optional_flag(mut self, condition: bool, flag: impl AsRef<OsStr>) -> Self {
512 if condition {
513 self.inner.arg(flag);
514 }
515 self
516 }
517
518 pub fn option(mut self, name: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> Self {
526 self.inner.arg(name);
527 self.inner.arg(value);
528 self
529 }
530
531 pub fn optional_option(
539 mut self,
540 name: impl AsRef<OsStr>,
541 value: Option<impl AsRef<OsStr>>,
542 ) -> Self {
543 if let Some(value) = value {
544 self.inner.arg(name);
545 self.inner.arg(value);
546 }
547 self
548 }
549
550 pub fn arguments<T: AsRef<OsStr>>(mut self, value: impl IntoIterator<Item = T>) -> Self {
551 self.inner.args(value);
552 self
553 }
554
555 pub fn working_directory(mut self, dir: impl AsRef<std::path::Path>) -> Self {
556 self.inner.current_dir(dir);
557 self
558 }
559
560 pub fn env(mut self, key: &EnvVariableName<'_>, val: impl AsRef<OsStr>) -> Self {
561 self.inner.env(key, val);
562 self
563 }
564
565 pub fn envs<'a, I, V>(mut self, vars: I) -> Self
566 where
567 I: IntoIterator<Item = (EnvVariableName<'a>, V)>,
568 V: AsRef<OsStr>,
569 {
570 for (key, val) in vars {
571 self.inner.env(key, val);
572 }
573 self
574 }
575
576 #[must_use]
578 pub fn env_remove(mut self, key: &EnvVariableName<'_>) -> Self {
579 self.inner.env_remove(key);
580 self
581 }
582
583 #[must_use]
584 pub fn stdin_bytes(mut self, data: impl Into<Vec<u8>>) -> Self {
585 self.stdin_data = Some(data.into());
586 self
587 }
588
589 #[must_use]
591 pub fn stdout_capture(self) -> CaptureSingle<Stdout> {
592 CaptureSingle {
593 inner: self.inner,
594 stdin_data: self.stdin_data,
595 accept_nonzero_exit: false,
596 _marker: PhantomData,
597 }
598 }
599
600 #[must_use]
602 pub fn stderr_capture(self) -> CaptureSingle<Stderr> {
603 CaptureSingle {
604 inner: self.inner,
605 stdin_data: self.stdin_data,
606 accept_nonzero_exit: false,
607 _marker: PhantomData,
608 }
609 }
610
611 #[must_use]
613 pub fn stdout_null(mut self) -> Self {
614 self.inner.stdout(std::process::Stdio::null());
615 self
616 }
617
618 #[must_use]
620 pub fn stderr_null(mut self) -> Self {
621 self.inner.stderr(std::process::Stdio::null());
622 self
623 }
624
625 #[must_use]
627 pub fn stdout_inherit(mut self) -> Self {
628 self.inner.stdout(std::process::Stdio::inherit());
629 self
630 }
631
632 #[must_use]
634 pub fn stderr_inherit(mut self) -> Self {
635 self.inner.stderr(std::process::Stdio::inherit());
636 self
637 }
638
639 #[must_use]
644 pub fn build(self) -> tokio::process::Command {
645 self.inner
646 }
647
648 pub async fn status(mut self) -> Result<(), CommandError> {
650 use std::process::Stdio;
651
652 log::debug!("{:#?}", self.inner);
653
654 if self.stdin_data.is_some() {
655 self.inner.stdin(Stdio::piped());
656 }
657
658 let start = std::time::Instant::now();
659
660 let child = self.inner.spawn().map_err(CommandError::Io)?;
661
662 let exit_status = run_and_wait_status(child, self.stdin_data, start).await?;
663
664 if exit_status.success() {
665 Ok(())
666 } else {
667 Err(CommandError::ExitStatus(exit_status))
668 }
669 }
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675
676 #[tokio::test]
677 async fn test_stdout_bytes_success() {
678 assert_eq!(
679 Command::new("echo")
680 .argument("hello")
681 .stdout_capture()
682 .bytes()
683 .await
684 .unwrap(),
685 b"hello\n"
686 );
687 }
688
689 #[tokio::test]
690 async fn test_stdout_bytes_nonzero_exit() {
691 let error = Command::new("sh")
692 .arguments(["-c", "exit 42"])
693 .stdout_capture()
694 .bytes()
695 .await
696 .unwrap_err();
697 let CommandError::ExitStatus(status) = error else {
698 panic!("expected ExitStatus, got {error:?}");
699 };
700 assert_eq!(status.code(), Some(42));
701 }
702
703 #[tokio::test]
704 async fn test_stdout_bytes_io_error() {
705 let error = Command::new("./nonexistent")
706 .stdout_capture()
707 .bytes()
708 .await
709 .unwrap_err();
710 let CommandError::Io(io_error) = error else {
711 panic!("expected Io, got {error:?}");
712 };
713 assert_eq!(io_error.kind(), std::io::ErrorKind::NotFound);
714 }
715
716 #[tokio::test]
717 async fn test_stdout_string_success() {
718 assert_eq!(
719 Command::new("echo")
720 .argument("hello")
721 .stdout_capture()
722 .string()
723 .await
724 .unwrap(),
725 "hello\n"
726 );
727 }
728
729 #[tokio::test]
730 async fn test_stderr_bytes_success() {
731 assert_eq!(
732 Command::new("sh")
733 .arguments(["-c", "echo error >&2"])
734 .stderr_capture()
735 .bytes()
736 .await
737 .unwrap(),
738 b"error\n"
739 );
740 }
741
742 #[tokio::test]
743 async fn test_stderr_string_success() {
744 assert_eq!(
745 Command::new("sh")
746 .arguments(["-c", "echo error >&2"])
747 .stderr_capture()
748 .string()
749 .await
750 .unwrap(),
751 "error\n"
752 );
753 }
754
755 #[tokio::test]
756 async fn test_status_success() {
757 assert!(Command::new("true").status().await.is_ok());
758 }
759
760 #[tokio::test]
761 async fn test_status_nonzero_exit() {
762 let error = Command::new("sh")
763 .arguments(["-c", "exit 42"])
764 .status()
765 .await
766 .unwrap_err();
767 let CommandError::ExitStatus(status) = error else {
768 panic!("expected ExitStatus, got {error:?}");
769 };
770 assert_eq!(status.code(), Some(42));
771 }
772
773 #[tokio::test]
774 async fn test_status_io_error() {
775 let error = Command::new("./nonexistent").status().await.unwrap_err();
776 let CommandError::Io(io_error) = error else {
777 panic!("expected Io, got {error:?}");
778 };
779 assert_eq!(io_error.kind(), std::io::ErrorKind::NotFound);
780 }
781
782 #[test]
783 fn test_env_variable_name_from_static_or_panic() {
784 const NAME: EnvVariableName<'static> = EnvVariableName::from_static_or_panic("PATH");
785 assert_eq!(NAME.as_str(), "PATH");
786 }
787
788 #[test]
789 fn test_env_variable_name_parse() {
790 let name: EnvVariableName = "HOME".parse().unwrap();
791 assert_eq!(name.as_str(), "HOME");
792 }
793
794 #[test]
795 fn test_env_variable_name_empty() {
796 let result: Result<EnvVariableName, _> = "".parse();
797 assert!(matches!(result, Err(EnvVariableNameError::Empty)));
798 }
799
800 #[test]
801 fn test_env_variable_name_contains_equals() {
802 let result: Result<EnvVariableName, _> = "FOO=BAR".parse();
803 assert!(matches!(result, Err(EnvVariableNameError::ContainsEquals)));
804 }
805
806 #[tokio::test]
807 async fn test_env_with_variable() {
808 let name: EnvVariableName = "MY_VAR".parse().unwrap();
809 let output = Command::new("sh")
810 .arguments(["-c", "echo $MY_VAR"])
811 .env(&name, "hello")
812 .stdout_capture()
813 .string()
814 .await
815 .unwrap();
816 assert_eq!(output, "hello\n");
817 }
818
819 #[tokio::test]
820 async fn test_stdin_bytes() {
821 let output = Command::new("cat")
822 .stdin_bytes(b"hello world".as_slice())
823 .stdout_capture()
824 .string()
825 .await
826 .unwrap();
827 assert_eq!(output, "hello world");
828 }
829
830 #[tokio::test]
831 async fn test_stdin_bytes_vec() {
832 let output = Command::new("cat")
833 .stdin_bytes(vec![104, 105])
834 .stdout_capture()
835 .string()
836 .await
837 .unwrap();
838 assert_eq!(output, "hi");
839 }
840
841 #[tokio::test]
842 async fn test_capture_all_success() {
843 let result = Command::new("echo")
844 .argument("hello")
845 .stdout_capture()
846 .stderr_capture()
847 .run()
848 .await
849 .unwrap();
850 assert!(result.status.success());
851 assert_eq!(result.stdout, b"hello\n");
852 assert!(result.stderr.is_empty());
853 }
854
855 #[tokio::test]
856 async fn test_capture_all_failure_with_stderr() {
857 let result = Command::new("sh")
858 .arguments(["-c", "echo error >&2; exit 1"])
859 .stdout_capture()
860 .stderr_capture()
861 .accept_nonzero_exit()
862 .run()
863 .await
864 .unwrap();
865 assert!(!result.status.success());
866 assert_eq!(String::from_utf8(result.stderr).unwrap(), "error\n");
867 }
868
869 #[tokio::test]
870 async fn test_capture_all_io_error() {
871 let error = Command::new("./nonexistent")
872 .stdout_capture()
873 .stderr_capture()
874 .run()
875 .await
876 .unwrap_err();
877 let CommandError::Io(io_error) = error else {
878 panic!("expected Io, got {error:?}");
879 };
880 assert_eq!(io_error.kind(), std::io::ErrorKind::NotFound);
881 }
882
883 #[tokio::test]
884 async fn test_build() {
885 use tokio::io::{AsyncReadExt, AsyncWriteExt};
886
887 let mut child = Command::new("cat")
888 .build()
889 .stdin(std::process::Stdio::piped())
890 .stdout(std::process::Stdio::piped())
891 .spawn()
892 .unwrap();
893
894 child
895 .stdin
896 .as_mut()
897 .unwrap()
898 .write_all(b"hello")
899 .await
900 .unwrap();
901 drop(child.stdin.take());
902
903 let mut output = String::new();
904 child
905 .stdout
906 .as_mut()
907 .unwrap()
908 .read_to_string(&mut output)
909 .await
910 .unwrap();
911 assert_eq!(output, "hello");
912
913 let status = child.wait().await.unwrap();
914 assert!(status.success());
915 }
916
917 #[tokio::test]
918 async fn test_option() {
919 let output = Command::new("echo")
920 .option("-n", "hello")
921 .stdout_capture()
922 .string()
923 .await
924 .unwrap();
925 assert_eq!(output, "hello");
926 }
927
928 #[tokio::test]
929 async fn test_optional_option_some() {
930 let output = Command::new("echo")
931 .optional_option("-n", Some("hello"))
932 .stdout_capture()
933 .string()
934 .await
935 .unwrap();
936 assert_eq!(output, "hello");
937 }
938
939 #[tokio::test]
940 async fn test_optional_option_none() {
941 let output = Command::new("echo")
942 .optional_option("-n", None::<&str>)
943 .argument("hello")
944 .stdout_capture()
945 .string()
946 .await
947 .unwrap();
948 assert_eq!(output, "hello\n");
949 }
950
951 #[tokio::test]
952 async fn test_optional_flag_true() {
953 let output = Command::new("echo")
954 .optional_flag(true, "-n")
955 .argument("hello")
956 .stdout_capture()
957 .string()
958 .await
959 .unwrap();
960 assert_eq!(output, "hello");
961 }
962
963 #[tokio::test]
964 async fn test_optional_flag_false() {
965 let output = Command::new("echo")
966 .optional_flag(false, "-n")
967 .argument("hello")
968 .stdout_capture()
969 .string()
970 .await
971 .unwrap();
972 assert_eq!(output, "hello\n");
973 }
974
975 #[tokio::test]
976 async fn test_stdout_null() {
977 Command::new("echo")
979 .argument("hello")
980 .stdout_null()
981 .status()
982 .await
983 .unwrap();
984 }
985
986 #[tokio::test]
987 async fn test_stderr_null() {
988 Command::new("sh")
990 .arguments(["-c", "echo error >&2"])
991 .stderr_null()
992 .status()
993 .await
994 .unwrap();
995 }
996
997 #[tokio::test]
998 async fn test_stdout_capture_stderr_null() {
999 let output = Command::new("sh")
1000 .arguments(["-c", "echo out; echo err >&2"])
1001 .stdout_capture()
1002 .stderr_null()
1003 .string()
1004 .await
1005 .unwrap();
1006 assert_eq!(output, "out\n");
1007 }
1008
1009 #[tokio::test]
1010 async fn test_accept_nonzero_exit_stdout() {
1011 let result = Command::new("sh")
1012 .arguments(["-c", "echo out; exit 42"])
1013 .stdout_capture()
1014 .accept_nonzero_exit()
1015 .run()
1016 .await
1017 .unwrap();
1018 assert!(!result.status.success());
1019 assert_eq!(result.bytes, b"out\n");
1020 }
1021
1022 #[tokio::test]
1023 async fn test_accept_nonzero_exit_capture_all() {
1024 let result = Command::new("sh")
1025 .arguments(["-c", "echo out; echo err >&2; exit 42"])
1026 .stdout_capture()
1027 .stderr_capture()
1028 .accept_nonzero_exit()
1029 .run()
1030 .await
1031 .unwrap();
1032 assert!(!result.status.success());
1033 assert_eq!(result.stdout, b"out\n");
1034 assert_eq!(result.stderr, b"err\n");
1035 }
1036}