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