Skip to main content

cmd_proc/
lib.rs

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/// Validated environment variable name.
67///
68/// Ensures the name is non-empty and does not contain `=`.
69#[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    /// Validated environment variable name for `'static` inputs.
93    ///
94    /// # Panics
95    ///
96    /// Panics at compile time when used in a `const` context, or at runtime otherwise,
97    /// if the name is empty or contains `=`.
98    #[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    // Iterator helpers are not const-stable yet; use a manual loop.
136    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
149/// Marker trait for stream type parameters.
150pub trait StreamMarker: sealed::Sealed {}
151
152/// Marker type for stdout stream.
153pub struct Stdout;
154
155/// Marker type for stderr stream.
156pub struct Stderr;
157
158impl sealed::Sealed for Stdout {}
159impl sealed::Sealed for Stderr {}
160impl StreamMarker for Stdout {}
161impl StreamMarker for Stderr {}
162
163/// Result from capturing a single stream.
164#[derive(Debug)]
165pub struct CaptureSingleResult {
166    pub bytes: Vec<u8>,
167    pub status: std::process::ExitStatus,
168}
169
170/// Result from capturing both stdout and stderr.
171#[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
201/// Builder for capturing a single stream from a command.
202///
203/// The type parameter `S` indicates which stream is being captured:
204/// `Stdout` or `Stderr`.
205pub 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    /// Accept non-zero exit status instead of treating it as an error.
214    #[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    /// Also capture stderr, transitioning to [`CaptureAll`].
223    #[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    /// Redirect stderr to /dev/null.
235    #[must_use]
236    pub fn stderr_null(mut self) -> Self {
237        self.inner.stderr(std::process::Stdio::null());
238        self
239    }
240
241    /// Inherit stderr from the parent process (default).
242    #[must_use]
243    pub fn stderr_inherit(mut self) -> Self {
244        self.inner.stderr(std::process::Stdio::inherit());
245        self
246    }
247
248    /// Stop capturing stdout (null), transitioning back to [`Command`].
249    #[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    /// Stop capturing stdout (inherit), transitioning back to [`Command`].
259    #[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    /// Execute the command and return captured stdout.
269    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    /// Execute the command and return captured stdout as bytes.
286    pub async fn bytes(self) -> Result<Vec<u8>, CommandError> {
287        Ok(self.run().await?.bytes)
288    }
289
290    /// Execute the command and return captured stdout as a UTF-8 string.
291    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    /// Also capture stdout, transitioning to [`CaptureAll`].
304    #[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    /// Redirect stdout to /dev/null.
316    #[must_use]
317    pub fn stdout_null(mut self) -> Self {
318        self.inner.stdout(std::process::Stdio::null());
319        self
320    }
321
322    /// Inherit stdout from the parent process (default).
323    #[must_use]
324    pub fn stdout_inherit(mut self) -> Self {
325        self.inner.stdout(std::process::Stdio::inherit());
326        self
327    }
328
329    /// Stop capturing stderr (null), transitioning back to [`Command`].
330    #[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    /// Stop capturing stderr (inherit), transitioning back to [`Command`].
340    #[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    /// Execute the command and return captured stderr.
350    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    /// Execute the command and return captured stderr as bytes.
367    pub async fn bytes(self) -> Result<Vec<u8>, CommandError> {
368        Ok(self.run().await?.bytes)
369    }
370
371    /// Execute the command and return captured stderr as a UTF-8 string.
372    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
383/// Builder for capturing both stdout and stderr from a command.
384pub struct CaptureAll {
385    inner: tokio::process::Command,
386    stdin_data: Option<Vec<u8>>,
387    accept_nonzero_exit: bool,
388}
389
390impl CaptureAll {
391    /// Accept non-zero exit status instead of treating it as an error.
392    #[must_use]
393    pub fn accept_nonzero_exit(mut self) -> Self {
394        self.accept_nonzero_exit = true;
395        self
396    }
397
398    /// Stop capturing stdout (null), transitioning to [`CaptureSingle<Stderr>`].
399    #[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    /// Stop capturing stdout (inherit), transitioning to [`CaptureSingle<Stderr>`].
411    #[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    /// Stop capturing stderr (null), transitioning to [`CaptureSingle<Stdout>`].
423    #[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    /// Stop capturing stderr (inherit), transitioning to [`CaptureSingle<Stdout>`].
435    #[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    /// Execute the command and return captured output from both streams.
447    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    /// Asserts that two commands are equal by comparing their `Debug` representations.
480    ///
481    /// This is useful for testing that a command builder produces the expected command
482    /// without actually executing it.
483    ///
484    /// # Panics
485    ///
486    /// Panics if the `Debug` output of the two commands' inner `tokio::process::Command` differ.
487    #[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    /// Adds a flag argument only if the condition is `true`.
505    ///
506    /// ```rust
507    /// let verbose = true;
508    /// cmd_proc::Command::new("echo")
509    ///     .optional_flag(verbose, "--verbose");
510    /// ```
511    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    /// Adds a CLI option (name + value).
519    ///
520    /// ```rust
521    /// cmd_proc::Command::new("git")
522    ///     .option("--message", "hello");
523    /// // equivalent to: .argument("--message").argument("hello")
524    /// ```
525    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    /// Adds a CLI option (name + value) only if the value is `Some`.
532    ///
533    /// ```rust
534    /// cmd_proc::Command::new("git")
535    ///     .optional_option("--author", Some("Alice"));
536    /// // adds "--author Alice" only if Some
537    /// ```
538    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    /// Remove an environment variable from the child process.
577    #[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    /// Capture stdout from this command.
590    #[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    /// Capture stderr from this command.
601    #[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    /// Redirect stdout to /dev/null.
612    #[must_use]
613    pub fn stdout_null(mut self) -> Self {
614        self.inner.stdout(std::process::Stdio::null());
615        self
616    }
617
618    /// Redirect stderr to /dev/null.
619    #[must_use]
620    pub fn stderr_null(mut self) -> Self {
621        self.inner.stderr(std::process::Stdio::null());
622        self
623    }
624
625    /// Inherit stdout from the parent process (default).
626    #[must_use]
627    pub fn stdout_inherit(mut self) -> Self {
628        self.inner.stdout(std::process::Stdio::inherit());
629        self
630    }
631
632    /// Inherit stderr from the parent process (default).
633    #[must_use]
634    pub fn stderr_inherit(mut self) -> Self {
635        self.inner.stderr(std::process::Stdio::inherit());
636        self
637    }
638
639    /// Consume the builder and return the underlying `tokio::process::Command`.
640    ///
641    /// Use this when you need full control over the child process (e.g.
642    /// interactive stdin/stdout piping) beyond what the capture API provides.
643    #[must_use]
644    pub fn build(self) -> tokio::process::Command {
645        self.inner
646    }
647
648    /// Execute the command and return success or an error.
649    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        // stdout_null should discard output; command should still succeed
978        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        // stderr_null should discard stderr; command should still succeed
989        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}