cmd_proc/
lib.rs

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/// Validated environment variable name.
71///
72/// Ensures the name is non-empty and does not contain `=`.
73#[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    /// Validated environment variable name for `'static` inputs.
97    ///
98    /// # Panics
99    ///
100    /// Panics at compile time when used in a `const` context, or at runtime otherwise,
101    /// if the name is empty or contains `=`.
102    #[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    // Iterator helpers are not const-stable yet; use a manual loop.
140    while i < bytes.len() {
141        if bytes[i] == b'=' {
142            return Err(EnvVariableNameError::ContainsEquals);
143        }
144        i += 1;
145    }
146    Ok(())
147}
148
149/// Output from a command execution, including both streams and exit status.
150///
151/// Unlike `Capture`, this does not treat non-zero exit as an error.
152/// Use this when you need to inspect stderr on failure.
153#[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    /// Returns true if the command exited successfully.
162    #[must_use]
163    pub fn success(&self) -> bool {
164        self.status.success()
165    }
166
167    /// Converts stdout to a UTF-8 string.
168    ///
169    /// # Errors
170    ///
171    /// Returns an error if stdout is not valid UTF-8.
172    pub fn into_stdout_string(self) -> Result<String, std::string::FromUtf8Error> {
173        String::from_utf8(self.stdout)
174    }
175
176    /// Converts stderr to a UTF-8 string.
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if stderr is not valid UTF-8.
181    pub fn into_stderr_string(self) -> Result<String, std::string::FromUtf8Error> {
182        String::from_utf8(self.stderr)
183    }
184}
185
186/// Which stream to capture from a command.
187#[derive(Clone, Copy)]
188enum CaptureStream {
189    Stdout,
190    Stderr,
191}
192
193/// Stdio configuration for spawned processes.
194#[derive(Clone, Copy, Default)]
195pub enum Stdio {
196    /// Pipe the stream, allowing reading/writing from the parent process.
197    Piped,
198    /// Inherit the stream from the parent process.
199    #[default]
200    Inherit,
201    /// Redirect to /dev/null.
202    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
215/// Builder for spawning a child process.
216///
217/// Created by [`Command::spawn`]. Configure stdin/stdout/stderr, then call [`Spawn::run`].
218pub 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    /// Configure stdin for the child process.
236    #[must_use]
237    pub fn stdin(mut self, stdio: Stdio) -> Self {
238        self.stdin = stdio;
239        self
240    }
241
242    /// Configure stdout for the child process.
243    #[must_use]
244    pub fn stdout(mut self, stdio: Stdio) -> Self {
245        self.stdout = stdio;
246        self
247    }
248
249    /// Configure stderr for the child process.
250    #[must_use]
251    pub fn stderr(mut self, stdio: Stdio) -> Self {
252        self.stderr = stdio;
253        self
254    }
255
256    /// Spawn the child process.
257    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/// A running child process.
278///
279/// Created by [`Spawn::run`].
280#[derive(Debug)]
281pub struct Child {
282    inner: std::process::Child,
283}
284
285impl Child {
286    /// Returns a mutable reference to the child's stdin handle.
287    pub fn stdin(&mut self) -> Option<&mut std::process::ChildStdin> {
288        self.inner.stdin.as_mut()
289    }
290
291    /// Returns a mutable reference to the child's stdout handle.
292    pub fn stdout(&mut self) -> Option<&mut std::process::ChildStdout> {
293        self.inner.stdout.as_mut()
294    }
295
296    /// Returns a mutable reference to the child's stderr handle.
297    pub fn stderr(&mut self) -> Option<&mut std::process::ChildStderr> {
298        self.inner.stderr.as_mut()
299    }
300
301    /// Takes ownership of the child's stdin handle.
302    pub fn take_stdin(&mut self) -> Option<std::process::ChildStdin> {
303        self.inner.stdin.take()
304    }
305
306    /// Takes ownership of the child's stdout handle.
307    pub fn take_stdout(&mut self) -> Option<std::process::ChildStdout> {
308        self.inner.stdout.take()
309    }
310
311    /// Takes ownership of the child's stderr handle.
312    pub fn take_stderr(&mut self) -> Option<std::process::ChildStderr> {
313        self.inner.stderr.take()
314    }
315
316    /// Waits for the child to exit and returns its exit status.
317    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    /// Simultaneously waits for the child to exit and collects all output.
325    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
342/// Builder for capturing command output.
343pub 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    /// Execute the command and return captured output as bytes.
354    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    /// Execute the command and return captured output as a UTF-8 string.
393    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    /// Asserts that two commands are equal by comparing their `Debug` representations.
419    ///
420    /// This is useful for testing that a command builder produces the expected command
421    /// without actually executing it.
422    ///
423    /// # Panics
424    ///
425    /// Panics if the `Debug` output of the two commands' inner `std::process::Command` differ.
426    #[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    /// Adds a CLI option (name + value).
444    ///
445    /// ```ignore
446    /// command.option("--message", "hello")
447    /// // equivalent to: command.argument("--message").argument("hello")
448    /// ```
449    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    /// Adds a CLI option (name + value) only if the value is `Some`.
456    ///
457    /// ```ignore
458    /// command.optional_option("--name", optional)
459    /// // adds "--name <value>" only if optional is Some
460    /// ```
461    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    /// Remove an environment variable from the child process.
500    #[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    /// Capture stdout from this command.
513    #[must_use]
514    pub fn stdout(self) -> Capture {
515        Capture::new(self, CaptureStream::Stdout)
516    }
517
518    /// Capture stderr from this command.
519    #[must_use]
520    pub fn stderr(self) -> Capture {
521        Capture::new(self, CaptureStream::Stderr)
522    }
523
524    /// Spawn the command as a child process.
525    ///
526    /// Returns a [`Spawn`] builder to configure stdin/stdout/stderr before running.
527    ///
528    /// # Example
529    ///
530    /// ```no_run
531    /// use cmd_proc::{Command, Stdio};
532    /// use std::io::BufRead;
533    ///
534    /// let mut child = Command::new("server")
535    ///     .argument("--port=8080")
536    ///     .spawn()
537    ///     .stdin(Stdio::Piped)
538    ///     .stdout(Stdio::Piped)
539    ///     .stderr(Stdio::Inherit)
540    ///     .run()
541    ///     .unwrap();
542    ///
543    /// // Read a line from stdout
544    /// let line = std::io::BufReader::new(child.stdout().unwrap())
545    ///     .lines()
546    ///     .next()
547    ///     .unwrap()
548    ///     .unwrap();
549    ///
550    /// // Close stdin to signal the child to exit
551    /// drop(child.take_stdin());
552    /// child.wait().unwrap();
553    /// ```
554    #[must_use]
555    pub fn spawn(self) -> Spawn {
556        Spawn::new(self)
557    }
558
559    /// Execute the command and return full output regardless of exit status.
560    ///
561    /// Unlike `stdout()` and `stderr()`, this does not treat non-zero exit as an error.
562    /// Use this when you need to inspect both streams or handle failure cases with stderr.
563    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    /// Execute the command and return success or an error.
592    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}