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