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
11async fn write_stdin(
12    child: &mut tokio::process::Child,
13    stdin_data: Option<Vec<u8>>,
14) -> Result<(), CommandError> {
15    use tokio::io::AsyncWriteExt;
16
17    if let Some(data) = stdin_data {
18        child
19            .stdin
20            .take()
21            .unwrap()
22            .write_all(&data)
23            .await
24            .map_err(|io_error| CommandError {
25                io_error: Some(io_error),
26                exit_status: None,
27            })?;
28    }
29
30    Ok(())
31}
32
33async fn run_and_wait(
34    mut child: tokio::process::Child,
35    stdin_data: Option<Vec<u8>>,
36    start: std::time::Instant,
37) -> Result<std::process::Output, CommandError> {
38    write_stdin(&mut child, stdin_data).await?;
39
40    let output = child
41        .wait_with_output()
42        .await
43        .map_err(|io_error| CommandError {
44            io_error: Some(io_error),
45            exit_status: None,
46        })?;
47
48    log::debug!(
49        "exit_status={:?} runtime={:?}",
50        output.status,
51        start.elapsed()
52    );
53
54    Ok(output)
55}
56
57async fn run_and_wait_status(
58    mut child: tokio::process::Child,
59    stdin_data: Option<Vec<u8>>,
60    start: std::time::Instant,
61) -> Result<std::process::ExitStatus, CommandError> {
62    write_stdin(&mut child, stdin_data).await?;
63
64    let status = child.wait().await.map_err(|io_error| CommandError {
65        io_error: Some(io_error),
66        exit_status: None,
67    })?;
68
69    log::debug!("exit_status={:?} runtime={:?}", status, start.elapsed());
70
71    Ok(status)
72}
73
74/// Validated environment variable name.
75///
76/// Ensures the name is non-empty and does not contain `=`.
77#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
78pub struct EnvVariableName<'a>(Cow<'a, str>);
79
80impl EnvVariableName<'_> {
81    #[must_use]
82    pub fn as_str(&self) -> &str {
83        &self.0
84    }
85}
86
87impl AsRef<OsStr> for EnvVariableName<'_> {
88    fn as_ref(&self) -> &OsStr {
89        self.0.as_ref().as_ref()
90    }
91}
92
93impl std::fmt::Display for EnvVariableName<'_> {
94    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        formatter.write_str(self.as_str())
96    }
97}
98
99impl EnvVariableName<'static> {
100    /// Validated environment variable name for `'static` inputs.
101    ///
102    /// # Panics
103    ///
104    /// Panics at compile time when used in a `const` context, or at runtime otherwise,
105    /// if the name is empty or contains `=`.
106    #[must_use]
107    pub const fn from_static_or_panic(s: &'static str) -> Self {
108        match validate_env_variable_name(s) {
109            Ok(()) => {}
110            Err(EnvVariableNameError::Empty) => {
111                panic!("Environment variable name cannot be empty");
112            }
113            Err(EnvVariableNameError::ContainsEquals) => {
114                panic!("Environment variable name cannot contain '='");
115            }
116        }
117        Self(Cow::Borrowed(s))
118    }
119}
120
121#[derive(Debug, thiserror::Error)]
122pub enum EnvVariableNameError {
123    #[error("Environment variable name cannot be empty")]
124    Empty,
125    #[error("Environment variable name cannot contain '='")]
126    ContainsEquals,
127}
128
129impl std::str::FromStr for EnvVariableName<'static> {
130    type Err = EnvVariableNameError;
131
132    fn from_str(s: &str) -> Result<Self, Self::Err> {
133        validate_env_variable_name(s).map(|()| Self(Cow::Owned(s.to_string())))
134    }
135}
136
137const fn validate_env_variable_name(s: &str) -> Result<(), EnvVariableNameError> {
138    if s.is_empty() {
139        return Err(EnvVariableNameError::Empty);
140    }
141    let bytes = s.as_bytes();
142    let mut i = 0;
143    // Iterator helpers are not const-stable yet; use a manual loop.
144    while i < bytes.len() {
145        if bytes[i] == b'=' {
146            return Err(EnvVariableNameError::ContainsEquals);
147        }
148        i += 1;
149    }
150    Ok(())
151}
152
153/// Output from a command execution, including both streams and exit status.
154///
155/// Unlike `Capture`, this does not treat non-zero exit as an error.
156/// Use this when you need to inspect stderr on failure.
157#[derive(Debug)]
158pub struct Output {
159    pub stdout: Vec<u8>,
160    pub stderr: Vec<u8>,
161    pub status: std::process::ExitStatus,
162}
163
164impl Output {
165    /// Returns true if the command exited successfully.
166    #[must_use]
167    pub fn success(&self) -> bool {
168        self.status.success()
169    }
170
171    /// Converts stdout to a UTF-8 string.
172    ///
173    /// # Errors
174    ///
175    /// Returns an error if stdout is not valid UTF-8.
176    pub fn into_stdout_string(self) -> Result<String, std::string::FromUtf8Error> {
177        String::from_utf8(self.stdout)
178    }
179
180    /// Converts stderr to a UTF-8 string.
181    ///
182    /// # Errors
183    ///
184    /// Returns an error if stderr is not valid UTF-8.
185    pub fn into_stderr_string(self) -> Result<String, std::string::FromUtf8Error> {
186        String::from_utf8(self.stderr)
187    }
188}
189
190/// Which stream to capture from a command.
191#[derive(Clone, Copy)]
192enum CaptureSingleStream {
193    Stdout,
194    Stderr,
195}
196
197/// Stdio configuration for spawned processes.
198#[derive(Clone, Copy, Default)]
199pub enum Stdio {
200    /// Pipe the stream, allowing reading/writing from the parent process.
201    Piped,
202    /// Inherit the stream from the parent process.
203    #[default]
204    Inherit,
205    /// Redirect to /dev/null.
206    Null,
207}
208
209impl From<Stdio> for std::process::Stdio {
210    fn from(stdio: Stdio) -> Self {
211        match stdio {
212            Stdio::Piped => std::process::Stdio::piped(),
213            Stdio::Inherit => std::process::Stdio::inherit(),
214            Stdio::Null => std::process::Stdio::null(),
215        }
216    }
217}
218
219/// Builder for spawning a child process.
220///
221/// Created by [`Command::spawn`]. Configure stdin/stdout/stderr, then call [`Spawn::run`].
222pub struct Spawn {
223    command: Command,
224    stdin: Stdio,
225    stdout: Stdio,
226    stderr: Stdio,
227}
228
229impl Spawn {
230    fn new(command: Command) -> Self {
231        Self {
232            command,
233            stdin: Stdio::Inherit,
234            stdout: Stdio::Inherit,
235            stderr: Stdio::Inherit,
236        }
237    }
238
239    /// Configure stdin for the child process.
240    #[must_use]
241    pub fn stdin(mut self, stdio: Stdio) -> Self {
242        self.stdin = stdio;
243        self
244    }
245
246    /// Configure stdout for the child process.
247    #[must_use]
248    pub fn stdout(mut self, stdio: Stdio) -> Self {
249        self.stdout = stdio;
250        self
251    }
252
253    /// Configure stderr for the child process.
254    #[must_use]
255    pub fn stderr(mut self, stdio: Stdio) -> Self {
256        self.stderr = stdio;
257        self
258    }
259
260    /// Spawn the child process.
261    pub fn run(mut self) -> Result<Child, CommandError> {
262        log::debug!("{:#?}", self.command.inner);
263
264        self.command.inner.stdin(self.stdin);
265        self.command.inner.stdout(self.stdout);
266        self.command.inner.stderr(self.stderr);
267
268        let inner = self
269            .command
270            .inner
271            .spawn()
272            .map_err(|io_error| CommandError {
273                io_error: Some(io_error),
274                exit_status: None,
275            })?;
276
277        Ok(Child { inner })
278    }
279}
280
281/// A running child process.
282///
283/// Created by [`Spawn::run`].
284#[derive(Debug)]
285pub struct Child {
286    inner: tokio::process::Child,
287}
288
289impl Child {
290    /// Returns a mutable reference to the child's stdin handle.
291    pub fn stdin(&mut self) -> Option<&mut tokio::process::ChildStdin> {
292        self.inner.stdin.as_mut()
293    }
294
295    /// Returns a mutable reference to the child's stdout handle.
296    pub fn stdout(&mut self) -> Option<&mut tokio::process::ChildStdout> {
297        self.inner.stdout.as_mut()
298    }
299
300    /// Returns a mutable reference to the child's stderr handle.
301    pub fn stderr(&mut self) -> Option<&mut tokio::process::ChildStderr> {
302        self.inner.stderr.as_mut()
303    }
304
305    /// Takes ownership of the child's stdin handle.
306    pub fn take_stdin(&mut self) -> Option<tokio::process::ChildStdin> {
307        self.inner.stdin.take()
308    }
309
310    /// Takes ownership of the child's stdout handle.
311    pub fn take_stdout(&mut self) -> Option<tokio::process::ChildStdout> {
312        self.inner.stdout.take()
313    }
314
315    /// Takes ownership of the child's stderr handle.
316    pub fn take_stderr(&mut self) -> Option<tokio::process::ChildStderr> {
317        self.inner.stderr.take()
318    }
319
320    /// Waits for the child to exit and returns its exit status.
321    pub async fn wait(mut self) -> Result<std::process::ExitStatus, CommandError> {
322        self.inner.wait().await.map_err(|io_error| CommandError {
323            io_error: Some(io_error),
324            exit_status: None,
325        })
326    }
327
328    /// Simultaneously waits for the child to exit and collects all output.
329    pub async fn wait_with_output(self) -> Result<Output, CommandError> {
330        let output = self
331            .inner
332            .wait_with_output()
333            .await
334            .map_err(|io_error| CommandError {
335                io_error: Some(io_error),
336                exit_status: None,
337            })?;
338
339        Ok(Output {
340            stdout: output.stdout,
341            stderr: output.stderr,
342            status: output.status,
343        })
344    }
345}
346
347/// Builder for capturing command output.
348pub struct Capture {
349    command: Command,
350    stream: CaptureSingleStream,
351    accept_nonzero_exit: bool,
352}
353
354async fn run_capture(
355    mut command: Command,
356    accept_nonzero_exit: bool,
357) -> Result<std::process::Output, CommandError> {
358    log::debug!("{:#?}", command.inner);
359
360    if command.stdin_data.is_some() {
361        command.inner.stdin(std::process::Stdio::piped());
362    }
363
364    let start = std::time::Instant::now();
365
366    let child = command.inner.spawn().map_err(|io_error| CommandError {
367        io_error: Some(io_error),
368        exit_status: None,
369    })?;
370
371    let output = run_and_wait(child, command.stdin_data, start).await?;
372
373    if accept_nonzero_exit || output.status.success() {
374        Ok(output)
375    } else {
376        Err(CommandError {
377            io_error: None,
378            exit_status: Some(output.status),
379        })
380    }
381}
382
383impl Capture {
384    fn new(command: Command, stream: CaptureSingleStream) -> Self {
385        Self {
386            command,
387            stream,
388            accept_nonzero_exit: false,
389        }
390    }
391
392    /// Accept non-zero exit status instead of treating it as an error.
393    #[must_use]
394    pub fn accept_nonzero_exit(mut self) -> Self {
395        self.accept_nonzero_exit = true;
396        self
397    }
398
399    /// Execute the command and return captured output as bytes.
400    pub async fn bytes(mut self) -> Result<Vec<u8>, CommandError> {
401        use std::process::Stdio;
402
403        let (stdout, stderr) = match self.stream {
404            CaptureSingleStream::Stdout => (Stdio::piped(), Stdio::inherit()),
405            CaptureSingleStream::Stderr => (Stdio::inherit(), Stdio::piped()),
406        };
407
408        self.command.inner.stdout(stdout);
409        self.command.inner.stderr(stderr);
410
411        let output = run_capture(self.command, self.accept_nonzero_exit).await?;
412
413        Ok(match self.stream {
414            CaptureSingleStream::Stdout => output.stdout,
415            CaptureSingleStream::Stderr => output.stderr,
416        })
417    }
418
419    /// Execute the command and return captured output as a UTF-8 string.
420    pub async fn string(self) -> Result<String, CommandError> {
421        let bytes = self.bytes().await?;
422        String::from_utf8(bytes).map_err(|utf8_error| CommandError {
423            io_error: Some(std::io::Error::new(
424                std::io::ErrorKind::InvalidData,
425                utf8_error,
426            )),
427            exit_status: None,
428        })
429    }
430}
431
432/// Builder for capturing both stdout and stderr from a command.
433pub struct CaptureAllStreams {
434    command: Command,
435    accept_nonzero_exit: bool,
436}
437
438impl CaptureAllStreams {
439    fn new(command: Command) -> Self {
440        Self {
441            command,
442            accept_nonzero_exit: false,
443        }
444    }
445
446    /// Accept non-zero exit status instead of treating it as an error.
447    #[must_use]
448    pub fn accept_nonzero_exit(mut self) -> Self {
449        self.accept_nonzero_exit = true;
450        self
451    }
452
453    /// Execute the command and return captured output.
454    pub async fn output(mut self) -> Result<Output, CommandError> {
455        use std::process::Stdio;
456
457        self.command.inner.stdout(Stdio::piped());
458        self.command.inner.stderr(Stdio::piped());
459
460        let output = run_capture(self.command, self.accept_nonzero_exit).await?;
461
462        Ok(Output {
463            stdout: output.stdout,
464            stderr: output.stderr,
465            status: output.status,
466        })
467    }
468}
469
470pub struct Command {
471    inner: tokio::process::Command,
472    stdin_data: Option<Vec<u8>>,
473}
474
475impl Command {
476    pub fn new(value: impl AsRef<OsStr>) -> Self {
477        Command {
478            inner: tokio::process::Command::new(value),
479            stdin_data: None,
480        }
481    }
482
483    /// Asserts that two commands are equal by comparing their `Debug` representations.
484    ///
485    /// This is useful for testing that a command builder produces the expected command
486    /// without actually executing it.
487    ///
488    /// # Panics
489    ///
490    /// Panics if the `Debug` output of the two commands' inner `tokio::process::Command` differ.
491    #[cfg(feature = "test-utils")]
492    pub fn test_eq(&self, other: &Self) {
493        assert_eq!(format!("{:?}", self.inner), format!("{:?}", other.inner));
494    }
495
496    pub fn argument(mut self, value: impl AsRef<OsStr>) -> Self {
497        self.inner.arg(value);
498        self
499    }
500
501    pub fn optional_argument(mut self, optional: Option<impl AsRef<OsStr>>) -> Self {
502        if let Some(value) = optional {
503            self.inner.arg(value);
504        }
505        self
506    }
507
508    /// Adds a flag argument only if the condition is `true`.
509    ///
510    /// ```ignore
511    /// command.optional_flag(verbose, "--verbose")
512    /// // adds "--verbose" only if verbose is true
513    /// ```
514    pub fn optional_flag(mut self, condition: bool, flag: impl AsRef<OsStr>) -> Self {
515        if condition {
516            self.inner.arg(flag);
517        }
518        self
519    }
520
521    /// Adds a CLI option (name + value).
522    ///
523    /// ```ignore
524    /// command.option("--message", "hello")
525    /// // equivalent to: command.argument("--message").argument("hello")
526    /// ```
527    pub fn option(mut self, name: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> Self {
528        self.inner.arg(name);
529        self.inner.arg(value);
530        self
531    }
532
533    /// Adds a CLI option (name + value) only if the value is `Some`.
534    ///
535    /// ```ignore
536    /// command.optional_option("--name", optional)
537    /// // adds "--name <value>" only if optional is Some
538    /// ```
539    pub fn optional_option(
540        mut self,
541        name: impl AsRef<OsStr>,
542        value: Option<impl AsRef<OsStr>>,
543    ) -> Self {
544        if let Some(value) = value {
545            self.inner.arg(name);
546            self.inner.arg(value);
547        }
548        self
549    }
550
551    pub fn arguments<T: AsRef<OsStr>>(mut self, value: impl IntoIterator<Item = T>) -> Self {
552        self.inner.args(value);
553        self
554    }
555
556    pub fn working_directory(mut self, dir: impl AsRef<std::path::Path>) -> Self {
557        self.inner.current_dir(dir);
558        self
559    }
560
561    pub fn env(mut self, key: &EnvVariableName<'_>, val: impl AsRef<OsStr>) -> Self {
562        self.inner.env(key, val);
563        self
564    }
565
566    pub fn envs<'a, I, V>(mut self, vars: I) -> Self
567    where
568        I: IntoIterator<Item = (EnvVariableName<'a>, V)>,
569        V: AsRef<OsStr>,
570    {
571        for (key, val) in vars {
572            self.inner.env(key, val);
573        }
574        self
575    }
576
577    /// Remove an environment variable from the child process.
578    #[must_use]
579    pub fn env_remove(mut self, key: &EnvVariableName<'_>) -> Self {
580        self.inner.env_remove(key);
581        self
582    }
583
584    #[must_use]
585    pub fn stdin_bytes(mut self, data: impl Into<Vec<u8>>) -> Self {
586        self.stdin_data = Some(data.into());
587        self
588    }
589
590    /// Capture stdout from this command.
591    #[must_use]
592    pub fn capture_stdout(self) -> Capture {
593        Capture::new(self, CaptureSingleStream::Stdout)
594    }
595
596    /// Capture stderr from this command.
597    #[must_use]
598    pub fn capture_stderr(self) -> Capture {
599        Capture::new(self, CaptureSingleStream::Stderr)
600    }
601
602    /// Capture both stdout and stderr from this command.
603    #[must_use]
604    pub fn capture_stderr_stdout(self) -> CaptureAllStreams {
605        CaptureAllStreams::new(self)
606    }
607
608    /// Spawn the command as a child process.
609    ///
610    /// Returns a [`Spawn`] builder to configure stdin/stdout/stderr before running.
611    ///
612    /// # Example
613    ///
614    /// ```no_run
615    /// use cmd_proc::{Command, Stdio};
616    /// use tokio::io::AsyncBufReadExt;
617    ///
618    /// # async fn example() -> Result<(), cmd_proc::CommandError> {
619    /// let mut child = Command::new("server")
620    ///     .argument("--port=8080")
621    ///     .spawn()
622    ///     .stdin(Stdio::Piped)
623    ///     .stdout(Stdio::Piped)
624    ///     .stderr(Stdio::Inherit)
625    ///     .run()
626    ///     .unwrap();
627    ///
628    /// // Read a line from stdout
629    /// let mut line = String::new();
630    /// tokio::io::BufReader::new(child.stdout().unwrap())
631    ///     .read_line(&mut line)
632    ///     .await
633    ///     .unwrap();
634    ///
635    /// // Close stdin to signal the child to exit
636    /// drop(child.take_stdin());
637    /// child.wait().await.unwrap();
638    /// # Ok(())
639    /// # }
640    /// ```
641    #[must_use]
642    pub fn spawn(self) -> Spawn {
643        Spawn::new(self)
644    }
645
646    /// Execute the command and return full output regardless of exit status.
647    ///
648    /// Unlike `stdout()` and `stderr()`, this does not treat non-zero exit as an error.
649    /// Use this when you need to inspect both streams or handle failure cases with stderr.
650    pub async fn output(mut self) -> Result<Output, CommandError> {
651        use std::process::Stdio;
652
653        log::debug!("{:#?}", self.inner);
654
655        self.inner.stdout(Stdio::piped());
656        self.inner.stderr(Stdio::piped());
657
658        if self.stdin_data.is_some() {
659            self.inner.stdin(Stdio::piped());
660        }
661
662        let start = std::time::Instant::now();
663
664        let child = self.inner.spawn().map_err(|io_error| CommandError {
665            io_error: Some(io_error),
666            exit_status: None,
667        })?;
668
669        let output = run_and_wait(child, self.stdin_data, start).await?;
670
671        Ok(Output {
672            stdout: output.stdout,
673            stderr: output.stderr,
674            status: output.status,
675        })
676    }
677
678    /// Execute the command and return success or an error.
679    pub async fn status(mut self) -> Result<(), CommandError> {
680        use std::process::Stdio;
681
682        log::debug!("{:#?}", self.inner);
683
684        if self.stdin_data.is_some() {
685            self.inner.stdin(Stdio::piped());
686        }
687
688        let start = std::time::Instant::now();
689
690        let child = self.inner.spawn().map_err(|io_error| CommandError {
691            io_error: Some(io_error),
692            exit_status: None,
693        })?;
694
695        let exit_status = run_and_wait_status(child, self.stdin_data, start).await?;
696
697        if exit_status.success() {
698            Ok(())
699        } else {
700            Err(CommandError {
701                io_error: None,
702                exit_status: Some(exit_status),
703            })
704        }
705    }
706}
707
708#[cfg(test)]
709mod tests {
710    use super::*;
711
712    #[tokio::test]
713    async fn test_stdout_bytes_success() {
714        assert_eq!(
715            Command::new("echo")
716                .argument("hello")
717                .capture_stdout()
718                .bytes()
719                .await
720                .unwrap(),
721            b"hello\n"
722        );
723    }
724
725    #[tokio::test]
726    async fn test_stdout_bytes_nonzero_exit() {
727        let error = Command::new("sh")
728            .arguments(["-c", "exit 42"])
729            .capture_stdout()
730            .bytes()
731            .await
732            .unwrap_err();
733        assert_eq!(
734            error.exit_status.map(|status| status.code()),
735            Some(Some(42))
736        );
737        assert!(error.io_error.is_none());
738    }
739
740    #[tokio::test]
741    async fn test_stdout_bytes_io_error() {
742        let error = Command::new("./nonexistent")
743            .capture_stdout()
744            .bytes()
745            .await
746            .unwrap_err();
747        assert!(error.io_error.is_some());
748        assert_eq!(error.io_error.unwrap().kind(), std::io::ErrorKind::NotFound);
749        assert!(error.exit_status.is_none());
750    }
751
752    #[tokio::test]
753    async fn test_stdout_string_success() {
754        assert_eq!(
755            Command::new("echo")
756                .argument("hello")
757                .capture_stdout()
758                .string()
759                .await
760                .unwrap(),
761            "hello\n"
762        );
763    }
764
765    #[tokio::test]
766    async fn test_stderr_bytes_success() {
767        assert_eq!(
768            Command::new("sh")
769                .arguments(["-c", "echo error >&2"])
770                .capture_stderr()
771                .bytes()
772                .await
773                .unwrap(),
774            b"error\n"
775        );
776    }
777
778    #[tokio::test]
779    async fn test_stderr_string_success() {
780        assert_eq!(
781            Command::new("sh")
782                .arguments(["-c", "echo error >&2"])
783                .capture_stderr()
784                .string()
785                .await
786                .unwrap(),
787            "error\n"
788        );
789    }
790
791    #[tokio::test]
792    async fn test_status_success() {
793        assert!(Command::new("true").status().await.is_ok());
794    }
795
796    #[tokio::test]
797    async fn test_status_nonzero_exit() {
798        let error = Command::new("sh")
799            .arguments(["-c", "exit 42"])
800            .status()
801            .await
802            .unwrap_err();
803        assert_eq!(
804            error.exit_status.map(|status| status.code()),
805            Some(Some(42))
806        );
807        assert!(error.io_error.is_none());
808    }
809
810    #[tokio::test]
811    async fn test_status_io_error() {
812        let error = Command::new("./nonexistent").status().await.unwrap_err();
813        assert!(error.io_error.is_some());
814        assert_eq!(error.io_error.unwrap().kind(), std::io::ErrorKind::NotFound);
815        assert!(error.exit_status.is_none());
816    }
817
818    #[test]
819    fn test_env_variable_name_from_static_or_panic() {
820        const NAME: EnvVariableName<'static> = EnvVariableName::from_static_or_panic("PATH");
821        assert_eq!(NAME.as_str(), "PATH");
822    }
823
824    #[test]
825    fn test_env_variable_name_parse() {
826        let name: EnvVariableName = "HOME".parse().unwrap();
827        assert_eq!(name.as_str(), "HOME");
828    }
829
830    #[test]
831    fn test_env_variable_name_empty() {
832        let result: Result<EnvVariableName, _> = "".parse();
833        assert!(matches!(result, Err(EnvVariableNameError::Empty)));
834    }
835
836    #[test]
837    fn test_env_variable_name_contains_equals() {
838        let result: Result<EnvVariableName, _> = "FOO=BAR".parse();
839        assert!(matches!(result, Err(EnvVariableNameError::ContainsEquals)));
840    }
841
842    #[tokio::test]
843    async fn test_env_with_variable() {
844        let name: EnvVariableName = "MY_VAR".parse().unwrap();
845        let output = Command::new("sh")
846            .arguments(["-c", "echo $MY_VAR"])
847            .env(&name, "hello")
848            .capture_stdout()
849            .string()
850            .await
851            .unwrap();
852        assert_eq!(output, "hello\n");
853    }
854
855    #[tokio::test]
856    async fn test_stdin_bytes() {
857        let output = Command::new("cat")
858            .stdin_bytes(b"hello world".as_slice())
859            .capture_stdout()
860            .string()
861            .await
862            .unwrap();
863        assert_eq!(output, "hello world");
864    }
865
866    #[tokio::test]
867    async fn test_stdin_bytes_vec() {
868        let output = Command::new("cat")
869            .stdin_bytes(vec![104, 105])
870            .capture_stdout()
871            .string()
872            .await
873            .unwrap();
874        assert_eq!(output, "hi");
875    }
876
877    #[tokio::test]
878    async fn test_output_success() {
879        let output = Command::new("echo")
880            .argument("hello")
881            .output()
882            .await
883            .unwrap();
884        assert!(output.success());
885        assert_eq!(output.stdout, b"hello\n");
886        assert!(output.stderr.is_empty());
887    }
888
889    #[tokio::test]
890    async fn test_output_failure_with_stderr() {
891        let output = Command::new("sh")
892            .arguments(["-c", "echo error >&2; exit 1"])
893            .output()
894            .await
895            .unwrap();
896        assert!(!output.success());
897        assert_eq!(output.into_stderr_string().unwrap(), "error\n");
898    }
899
900    #[tokio::test]
901    async fn test_output_io_error() {
902        let error = Command::new("./nonexistent").output().await.unwrap_err();
903        assert!(error.io_error.is_some());
904        assert_eq!(error.io_error.unwrap().kind(), std::io::ErrorKind::NotFound);
905    }
906
907    #[tokio::test]
908    async fn test_spawn_with_piped_stdout() {
909        use tokio::io::AsyncReadExt;
910
911        let mut child = Command::new("echo")
912            .argument("hello")
913            .spawn()
914            .stdout(Stdio::Piped)
915            .run()
916            .unwrap();
917
918        let mut output = String::new();
919        child
920            .stdout()
921            .unwrap()
922            .read_to_string(&mut output)
923            .await
924            .unwrap();
925        assert_eq!(output, "hello\n");
926
927        let status = child.wait().await.unwrap();
928        assert!(status.success());
929    }
930
931    #[tokio::test]
932    async fn test_spawn_with_piped_stdin() {
933        use tokio::io::AsyncWriteExt;
934
935        let mut child = Command::new("cat")
936            .spawn()
937            .stdin(Stdio::Piped)
938            .stdout(Stdio::Piped)
939            .run()
940            .unwrap();
941
942        child.stdin().unwrap().write_all(b"hello").await.unwrap();
943        drop(child.take_stdin());
944
945        let output = child.wait_with_output().await.unwrap();
946        assert!(output.success());
947        assert_eq!(output.stdout, b"hello");
948    }
949
950    #[tokio::test]
951    async fn test_spawn_wait() {
952        let child = Command::new("true").spawn().run().unwrap();
953
954        let status = child.wait().await.unwrap();
955        assert!(status.success());
956    }
957
958    #[tokio::test]
959    async fn test_spawn_wait_with_output() {
960        let child = Command::new("sh")
961            .arguments(["-c", "echo out; echo err >&2"])
962            .spawn()
963            .stdout(Stdio::Piped)
964            .stderr(Stdio::Piped)
965            .run()
966            .unwrap();
967
968        let output = child.wait_with_output().await.unwrap();
969        assert!(output.success());
970        assert_eq!(output.stdout, b"out\n");
971        assert_eq!(output.stderr, b"err\n");
972    }
973
974    #[tokio::test]
975    async fn test_spawn_io_error() {
976        let error = Command::new("./nonexistent").spawn().run().unwrap_err();
977        assert!(error.io_error.is_some());
978        assert_eq!(error.io_error.unwrap().kind(), std::io::ErrorKind::NotFound);
979    }
980
981    #[tokio::test]
982    async fn test_spawn_take_handles() {
983        use tokio::io::{AsyncReadExt, AsyncWriteExt};
984
985        let mut child = Command::new("cat")
986            .spawn()
987            .stdin(Stdio::Piped)
988            .stdout(Stdio::Piped)
989            .run()
990            .unwrap();
991
992        let mut stdin = child.take_stdin().unwrap();
993        stdin.write_all(b"test").await.unwrap();
994        drop(stdin);
995
996        let mut stdout = child.take_stdout().unwrap();
997        let mut output = String::new();
998        stdout.read_to_string(&mut output).await.unwrap();
999        assert_eq!(output, "test");
1000
1001        child.wait().await.unwrap();
1002    }
1003
1004    #[tokio::test]
1005    async fn test_option() {
1006        let output = Command::new("echo")
1007            .option("-n", "hello")
1008            .capture_stdout()
1009            .string()
1010            .await
1011            .unwrap();
1012        assert_eq!(output, "hello");
1013    }
1014
1015    #[tokio::test]
1016    async fn test_optional_option_some() {
1017        let output = Command::new("echo")
1018            .optional_option("-n", Some("hello"))
1019            .capture_stdout()
1020            .string()
1021            .await
1022            .unwrap();
1023        assert_eq!(output, "hello");
1024    }
1025
1026    #[tokio::test]
1027    async fn test_optional_option_none() {
1028        let output = Command::new("echo")
1029            .optional_option("-n", None::<&str>)
1030            .argument("hello")
1031            .capture_stdout()
1032            .string()
1033            .await
1034            .unwrap();
1035        assert_eq!(output, "hello\n");
1036    }
1037
1038    #[tokio::test]
1039    async fn test_optional_flag_true() {
1040        let output = Command::new("echo")
1041            .optional_flag(true, "-n")
1042            .argument("hello")
1043            .capture_stdout()
1044            .string()
1045            .await
1046            .unwrap();
1047        assert_eq!(output, "hello");
1048    }
1049
1050    #[tokio::test]
1051    async fn test_optional_flag_false() {
1052        let output = Command::new("echo")
1053            .optional_flag(false, "-n")
1054            .argument("hello")
1055            .capture_stdout()
1056            .string()
1057            .await
1058            .unwrap();
1059        assert_eq!(output, "hello\n");
1060    }
1061}