Skip to main content

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 CaptureSingleStream {
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: CaptureSingleStream,
346    accept_nonzero_exit: bool,
347}
348
349fn run_capture(
350    mut command: Command,
351    accept_nonzero_exit: bool,
352) -> Result<std::process::Output, CommandError> {
353    log::debug!("{:#?}", command.inner);
354
355    if command.stdin_data.is_some() {
356        command.inner.stdin(std::process::Stdio::piped());
357    }
358
359    let start = std::time::Instant::now();
360
361    let child = command.inner.spawn().map_err(|io_error| CommandError {
362        io_error: Some(io_error),
363        exit_status: None,
364    })?;
365
366    let output = run_and_wait(child, command.stdin_data, start)?;
367
368    if accept_nonzero_exit || output.status.success() {
369        Ok(output)
370    } else {
371        Err(CommandError {
372            io_error: None,
373            exit_status: Some(output.status),
374        })
375    }
376}
377
378impl Capture {
379    fn new(command: Command, stream: CaptureSingleStream) -> Self {
380        Self {
381            command,
382            stream,
383            accept_nonzero_exit: false,
384        }
385    }
386
387    /// Accept non-zero exit status instead of treating it as an error.
388    #[must_use]
389    pub fn accept_nonzero_exit(mut self) -> Self {
390        self.accept_nonzero_exit = true;
391        self
392    }
393
394    /// Execute the command and return captured output as bytes.
395    pub fn bytes(mut self) -> Result<Vec<u8>, CommandError> {
396        use std::process::Stdio;
397
398        let (stdout, stderr) = match self.stream {
399            CaptureSingleStream::Stdout => (Stdio::piped(), Stdio::inherit()),
400            CaptureSingleStream::Stderr => (Stdio::inherit(), Stdio::piped()),
401        };
402
403        self.command.inner.stdout(stdout);
404        self.command.inner.stderr(stderr);
405
406        let output = run_capture(self.command, self.accept_nonzero_exit)?;
407
408        Ok(match self.stream {
409            CaptureSingleStream::Stdout => output.stdout,
410            CaptureSingleStream::Stderr => output.stderr,
411        })
412    }
413
414    /// Execute the command and return captured output as a UTF-8 string.
415    pub fn string(self) -> Result<String, CommandError> {
416        let bytes = self.bytes()?;
417        String::from_utf8(bytes).map_err(|utf8_error| CommandError {
418            io_error: Some(std::io::Error::new(
419                std::io::ErrorKind::InvalidData,
420                utf8_error,
421            )),
422            exit_status: None,
423        })
424    }
425}
426
427/// Builder for capturing both stdout and stderr from a command.
428pub struct CaptureAllStreams {
429    command: Command,
430    accept_nonzero_exit: bool,
431}
432
433impl CaptureAllStreams {
434    fn new(command: Command) -> Self {
435        Self {
436            command,
437            accept_nonzero_exit: false,
438        }
439    }
440
441    /// Accept non-zero exit status instead of treating it as an error.
442    #[must_use]
443    pub fn accept_nonzero_exit(mut self) -> Self {
444        self.accept_nonzero_exit = true;
445        self
446    }
447
448    /// Execute the command and return captured output.
449    pub fn output(mut self) -> Result<Output, CommandError> {
450        use std::process::Stdio;
451
452        self.command.inner.stdout(Stdio::piped());
453        self.command.inner.stderr(Stdio::piped());
454
455        let output = run_capture(self.command, self.accept_nonzero_exit)?;
456
457        Ok(Output {
458            stdout: output.stdout,
459            stderr: output.stderr,
460            status: output.status,
461        })
462    }
463}
464
465pub struct Command {
466    inner: std::process::Command,
467    stdin_data: Option<Vec<u8>>,
468}
469
470impl Command {
471    pub fn new(value: impl AsRef<OsStr>) -> Self {
472        Command {
473            inner: std::process::Command::new(value),
474            stdin_data: None,
475        }
476    }
477
478    /// Asserts that two commands are equal by comparing their `Debug` representations.
479    ///
480    /// This is useful for testing that a command builder produces the expected command
481    /// without actually executing it.
482    ///
483    /// # Panics
484    ///
485    /// Panics if the `Debug` output of the two commands' inner `std::process::Command` differ.
486    #[cfg(feature = "test-utils")]
487    pub fn test_eq(&self, other: &Self) {
488        assert_eq!(format!("{:?}", self.inner), format!("{:?}", other.inner));
489    }
490
491    pub fn argument(mut self, value: impl AsRef<OsStr>) -> Self {
492        self.inner.arg(value);
493        self
494    }
495
496    pub fn optional_argument(mut self, optional: Option<impl AsRef<OsStr>>) -> Self {
497        if let Some(value) = optional {
498            self.inner.arg(value);
499        }
500        self
501    }
502
503    /// Adds a flag argument only if the condition is `true`.
504    ///
505    /// ```ignore
506    /// command.optional_flag(verbose, "--verbose")
507    /// // adds "--verbose" only if verbose is true
508    /// ```
509    pub fn optional_flag(mut self, condition: bool, flag: impl AsRef<OsStr>) -> Self {
510        if condition {
511            self.inner.arg(flag);
512        }
513        self
514    }
515
516    /// Adds a CLI option (name + value).
517    ///
518    /// ```ignore
519    /// command.option("--message", "hello")
520    /// // equivalent to: command.argument("--message").argument("hello")
521    /// ```
522    pub fn option(mut self, name: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> Self {
523        self.inner.arg(name);
524        self.inner.arg(value);
525        self
526    }
527
528    /// Adds a CLI option (name + value) only if the value is `Some`.
529    ///
530    /// ```ignore
531    /// command.optional_option("--name", optional)
532    /// // adds "--name <value>" only if optional is Some
533    /// ```
534    pub fn optional_option(
535        mut self,
536        name: impl AsRef<OsStr>,
537        value: Option<impl AsRef<OsStr>>,
538    ) -> Self {
539        if let Some(value) = value {
540            self.inner.arg(name);
541            self.inner.arg(value);
542        }
543        self
544    }
545
546    pub fn arguments<T: AsRef<OsStr>>(mut self, value: impl IntoIterator<Item = T>) -> Self {
547        self.inner.args(value);
548        self
549    }
550
551    pub fn working_directory(mut self, dir: impl AsRef<std::path::Path>) -> Self {
552        self.inner.current_dir(dir);
553        self
554    }
555
556    pub fn env(mut self, key: &EnvVariableName<'_>, val: impl AsRef<OsStr>) -> Self {
557        self.inner.env(key, val);
558        self
559    }
560
561    pub fn envs<'a, I, V>(mut self, vars: I) -> Self
562    where
563        I: IntoIterator<Item = (EnvVariableName<'a>, V)>,
564        V: AsRef<OsStr>,
565    {
566        for (key, val) in vars {
567            self.inner.env(key, val);
568        }
569        self
570    }
571
572    /// Remove an environment variable from the child process.
573    #[must_use]
574    pub fn env_remove(mut self, key: &EnvVariableName<'_>) -> Self {
575        self.inner.env_remove(key);
576        self
577    }
578
579    #[must_use]
580    pub fn stdin_bytes(mut self, data: impl Into<Vec<u8>>) -> Self {
581        self.stdin_data = Some(data.into());
582        self
583    }
584
585    /// Capture stdout from this command.
586    #[must_use]
587    pub fn capture_stdout(self) -> Capture {
588        Capture::new(self, CaptureSingleStream::Stdout)
589    }
590
591    /// Capture stderr from this command.
592    #[must_use]
593    pub fn capture_stderr(self) -> Capture {
594        Capture::new(self, CaptureSingleStream::Stderr)
595    }
596
597    /// Capture both stdout and stderr from this command.
598    #[must_use]
599    pub fn capture_stderr_stdout(self) -> CaptureAllStreams {
600        CaptureAllStreams::new(self)
601    }
602
603    /// Spawn the command as a child process.
604    ///
605    /// Returns a [`Spawn`] builder to configure stdin/stdout/stderr before running.
606    ///
607    /// # Example
608    ///
609    /// ```no_run
610    /// use cmd_proc::{Command, Stdio};
611    /// use std::io::BufRead;
612    ///
613    /// let mut child = Command::new("server")
614    ///     .argument("--port=8080")
615    ///     .spawn()
616    ///     .stdin(Stdio::Piped)
617    ///     .stdout(Stdio::Piped)
618    ///     .stderr(Stdio::Inherit)
619    ///     .run()
620    ///     .unwrap();
621    ///
622    /// // Read a line from stdout
623    /// let line = std::io::BufReader::new(child.stdout().unwrap())
624    ///     .lines()
625    ///     .next()
626    ///     .unwrap()
627    ///     .unwrap();
628    ///
629    /// // Close stdin to signal the child to exit
630    /// drop(child.take_stdin());
631    /// child.wait().unwrap();
632    /// ```
633    #[must_use]
634    pub fn spawn(self) -> Spawn {
635        Spawn::new(self)
636    }
637
638    /// Execute the command and return full output regardless of exit status.
639    ///
640    /// Unlike `stdout()` and `stderr()`, this does not treat non-zero exit as an error.
641    /// Use this when you need to inspect both streams or handle failure cases with stderr.
642    pub fn output(mut self) -> Result<Output, CommandError> {
643        use std::process::Stdio;
644
645        log::debug!("{:#?}", self.inner);
646
647        self.inner.stdout(Stdio::piped());
648        self.inner.stderr(Stdio::piped());
649
650        if self.stdin_data.is_some() {
651            self.inner.stdin(Stdio::piped());
652        }
653
654        let start = std::time::Instant::now();
655
656        let child = self.inner.spawn().map_err(|io_error| CommandError {
657            io_error: Some(io_error),
658            exit_status: None,
659        })?;
660
661        let output = run_and_wait(child, self.stdin_data, start)?;
662
663        Ok(Output {
664            stdout: output.stdout,
665            stderr: output.stderr,
666            status: output.status,
667        })
668    }
669
670    /// Execute the command and return success or an error.
671    pub fn status(mut self) -> Result<(), CommandError> {
672        use std::process::Stdio;
673
674        log::debug!("{:#?}", self.inner);
675
676        if self.stdin_data.is_some() {
677            self.inner.stdin(Stdio::piped());
678        }
679
680        let start = std::time::Instant::now();
681
682        let child = self.inner.spawn().map_err(|io_error| CommandError {
683            io_error: Some(io_error),
684            exit_status: None,
685        })?;
686
687        let exit_status = run_and_wait_status(child, self.stdin_data, start)?;
688
689        if exit_status.success() {
690            Ok(())
691        } else {
692            Err(CommandError {
693                io_error: None,
694                exit_status: Some(exit_status),
695            })
696        }
697    }
698}
699
700#[cfg(test)]
701mod tests {
702    use super::*;
703
704    #[test]
705    fn test_stdout_bytes_success() {
706        assert_eq!(
707            Command::new("echo")
708                .argument("hello")
709                .capture_stdout()
710                .bytes()
711                .unwrap(),
712            b"hello\n"
713        );
714    }
715
716    #[test]
717    fn test_stdout_bytes_nonzero_exit() {
718        let error = Command::new("sh")
719            .arguments(["-c", "exit 42"])
720            .capture_stdout()
721            .bytes()
722            .unwrap_err();
723        assert_eq!(
724            error.exit_status.map(|status| status.code()),
725            Some(Some(42))
726        );
727        assert!(error.io_error.is_none());
728    }
729
730    #[test]
731    fn test_stdout_bytes_io_error() {
732        let error = Command::new("./nonexistent")
733            .capture_stdout()
734            .bytes()
735            .unwrap_err();
736        assert!(error.io_error.is_some());
737        assert_eq!(error.io_error.unwrap().kind(), std::io::ErrorKind::NotFound);
738        assert!(error.exit_status.is_none());
739    }
740
741    #[test]
742    fn test_stdout_string_success() {
743        assert_eq!(
744            Command::new("echo")
745                .argument("hello")
746                .capture_stdout()
747                .string()
748                .unwrap(),
749            "hello\n"
750        );
751    }
752
753    #[test]
754    fn test_stderr_bytes_success() {
755        assert_eq!(
756            Command::new("sh")
757                .arguments(["-c", "echo error >&2"])
758                .capture_stderr()
759                .bytes()
760                .unwrap(),
761            b"error\n"
762        );
763    }
764
765    #[test]
766    fn test_stderr_string_success() {
767        assert_eq!(
768            Command::new("sh")
769                .arguments(["-c", "echo error >&2"])
770                .capture_stderr()
771                .string()
772                .unwrap(),
773            "error\n"
774        );
775    }
776
777    #[test]
778    fn test_status_success() {
779        assert!(Command::new("true").status().is_ok());
780    }
781
782    #[test]
783    fn test_status_nonzero_exit() {
784        let error = Command::new("sh")
785            .arguments(["-c", "exit 42"])
786            .status()
787            .unwrap_err();
788        assert_eq!(
789            error.exit_status.map(|status| status.code()),
790            Some(Some(42))
791        );
792        assert!(error.io_error.is_none());
793    }
794
795    #[test]
796    fn test_status_io_error() {
797        let error = Command::new("./nonexistent").status().unwrap_err();
798        assert!(error.io_error.is_some());
799        assert_eq!(error.io_error.unwrap().kind(), std::io::ErrorKind::NotFound);
800        assert!(error.exit_status.is_none());
801    }
802
803    #[test]
804    fn test_env_variable_name_from_static_or_panic() {
805        const NAME: EnvVariableName<'static> = EnvVariableName::from_static_or_panic("PATH");
806        assert_eq!(NAME.as_str(), "PATH");
807    }
808
809    #[test]
810    fn test_env_variable_name_parse() {
811        let name: EnvVariableName = "HOME".parse().unwrap();
812        assert_eq!(name.as_str(), "HOME");
813    }
814
815    #[test]
816    fn test_env_variable_name_empty() {
817        let result: Result<EnvVariableName, _> = "".parse();
818        assert!(matches!(result, Err(EnvVariableNameError::Empty)));
819    }
820
821    #[test]
822    fn test_env_variable_name_contains_equals() {
823        let result: Result<EnvVariableName, _> = "FOO=BAR".parse();
824        assert!(matches!(result, Err(EnvVariableNameError::ContainsEquals)));
825    }
826
827    #[test]
828    fn test_env_with_variable() {
829        let name: EnvVariableName = "MY_VAR".parse().unwrap();
830        let output = Command::new("sh")
831            .arguments(["-c", "echo $MY_VAR"])
832            .env(&name, "hello")
833            .capture_stdout()
834            .string()
835            .unwrap();
836        assert_eq!(output, "hello\n");
837    }
838
839    #[test]
840    fn test_stdin_bytes() {
841        let output = Command::new("cat")
842            .stdin_bytes(b"hello world".as_slice())
843            .capture_stdout()
844            .string()
845            .unwrap();
846        assert_eq!(output, "hello world");
847    }
848
849    #[test]
850    fn test_stdin_bytes_vec() {
851        let output = Command::new("cat")
852            .stdin_bytes(vec![104, 105])
853            .capture_stdout()
854            .string()
855            .unwrap();
856        assert_eq!(output, "hi");
857    }
858
859    #[test]
860    fn test_output_success() {
861        let output = Command::new("echo").argument("hello").output().unwrap();
862        assert!(output.success());
863        assert_eq!(output.stdout, b"hello\n");
864        assert!(output.stderr.is_empty());
865    }
866
867    #[test]
868    fn test_output_failure_with_stderr() {
869        let output = Command::new("sh")
870            .arguments(["-c", "echo error >&2; exit 1"])
871            .output()
872            .unwrap();
873        assert!(!output.success());
874        assert_eq!(output.into_stderr_string().unwrap(), "error\n");
875    }
876
877    #[test]
878    fn test_output_io_error() {
879        let error = Command::new("./nonexistent").output().unwrap_err();
880        assert!(error.io_error.is_some());
881        assert_eq!(error.io_error.unwrap().kind(), std::io::ErrorKind::NotFound);
882    }
883
884    #[test]
885    fn test_spawn_with_piped_stdout() {
886        use std::io::Read;
887
888        let mut child = Command::new("echo")
889            .argument("hello")
890            .spawn()
891            .stdout(Stdio::Piped)
892            .run()
893            .unwrap();
894
895        let mut output = String::new();
896        child.stdout().unwrap().read_to_string(&mut output).unwrap();
897        assert_eq!(output, "hello\n");
898
899        let status = child.wait().unwrap();
900        assert!(status.success());
901    }
902
903    #[test]
904    fn test_spawn_with_piped_stdin() {
905        use std::io::Write;
906
907        let mut child = Command::new("cat")
908            .spawn()
909            .stdin(Stdio::Piped)
910            .stdout(Stdio::Piped)
911            .run()
912            .unwrap();
913
914        child.stdin().unwrap().write_all(b"hello").unwrap();
915        drop(child.take_stdin());
916
917        let output = child.wait_with_output().unwrap();
918        assert!(output.success());
919        assert_eq!(output.stdout, b"hello");
920    }
921
922    #[test]
923    fn test_spawn_wait() {
924        let child = Command::new("true").spawn().run().unwrap();
925
926        let status = child.wait().unwrap();
927        assert!(status.success());
928    }
929
930    #[test]
931    fn test_spawn_wait_with_output() {
932        let child = Command::new("sh")
933            .arguments(["-c", "echo out; echo err >&2"])
934            .spawn()
935            .stdout(Stdio::Piped)
936            .stderr(Stdio::Piped)
937            .run()
938            .unwrap();
939
940        let output = child.wait_with_output().unwrap();
941        assert!(output.success());
942        assert_eq!(output.stdout, b"out\n");
943        assert_eq!(output.stderr, b"err\n");
944    }
945
946    #[test]
947    fn test_spawn_io_error() {
948        let error = Command::new("./nonexistent").spawn().run().unwrap_err();
949        assert!(error.io_error.is_some());
950        assert_eq!(error.io_error.unwrap().kind(), std::io::ErrorKind::NotFound);
951    }
952
953    #[test]
954    fn test_spawn_take_handles() {
955        use std::io::{Read, Write};
956
957        let mut child = Command::new("cat")
958            .spawn()
959            .stdin(Stdio::Piped)
960            .stdout(Stdio::Piped)
961            .run()
962            .unwrap();
963
964        let mut stdin = child.take_stdin().unwrap();
965        stdin.write_all(b"test").unwrap();
966        drop(stdin);
967
968        let mut stdout = child.take_stdout().unwrap();
969        let mut output = String::new();
970        stdout.read_to_string(&mut output).unwrap();
971        assert_eq!(output, "test");
972
973        child.wait().unwrap();
974    }
975
976    #[test]
977    fn test_option() {
978        let output = Command::new("echo")
979            .option("-n", "hello")
980            .capture_stdout()
981            .string()
982            .unwrap();
983        assert_eq!(output, "hello");
984    }
985
986    #[test]
987    fn test_optional_option_some() {
988        let output = Command::new("echo")
989            .optional_option("-n", Some("hello"))
990            .capture_stdout()
991            .string()
992            .unwrap();
993        assert_eq!(output, "hello");
994    }
995
996    #[test]
997    fn test_optional_option_none() {
998        let output = Command::new("echo")
999            .optional_option("-n", None::<&str>)
1000            .argument("hello")
1001            .capture_stdout()
1002            .string()
1003            .unwrap();
1004        assert_eq!(output, "hello\n");
1005    }
1006
1007    #[test]
1008    fn test_optional_flag_true() {
1009        let output = Command::new("echo")
1010            .optional_flag(true, "-n")
1011            .argument("hello")
1012            .capture_stdout()
1013            .string()
1014            .unwrap();
1015        assert_eq!(output, "hello");
1016    }
1017
1018    #[test]
1019    fn test_optional_flag_false() {
1020        let output = Command::new("echo")
1021            .optional_flag(false, "-n")
1022            .argument("hello")
1023            .capture_stdout()
1024            .string()
1025            .unwrap();
1026        assert_eq!(output, "hello\n");
1027    }
1028}