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