process/
command.rs

1//! # Command
2//!
3//! Module dedicated to commands. It only exposes the [`Command`]
4//! struct, and various implementations of transformation.
5
6use std::{
7    ops::{Deref, DerefMut},
8    process::Stdio,
9};
10
11#[cfg(feature = "async-std")]
12use async_std::{io::WriteExt, process::Command as AsyncCommand};
13#[cfg(feature = "tokio")]
14use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
15use tracing::{debug, info};
16
17use crate::{Error, Output, Result};
18
19/// The command structure.
20///
21/// The structure is just a simple `String` wrapper.
22#[derive(Clone, Debug, Eq, PartialEq)]
23#[cfg_attr(
24    feature = "derive",
25    derive(serde::Serialize, serde::Deserialize),
26    serde(from = "String", into = "String")
27)]
28pub struct Command {
29    /// The inner command.
30    inner: String,
31
32    /// Whenever the output should be piped or not.
33    ///
34    /// Defaults to `true`.
35    #[cfg_attr(feature = "derive", serde(skip))]
36    piped: bool,
37}
38
39impl Command {
40    /// Creates a new command from a string.
41    ///
42    /// By default, the output is piped. Use
43    /// [`Command::with_output_piped`] to control this behaviour.
44    pub fn new(cmd: impl ToString) -> Self {
45        Self {
46            inner: cmd.to_string(),
47            piped: true,
48        }
49    }
50
51    /// Defines whenever the output should be piped or not.
52    ///
53    /// See [`Command::with_output_piped`] for the builder pattern
54    /// alternative.
55    pub fn set_output_piped(&mut self, piped: bool) {
56        self.piped = piped;
57    }
58
59    /// Defines whenever the output should be piped or not, using the
60    /// builder pattern.
61    ///
62    /// See [`Command::set_output_piped`] for the setter alternative.
63    pub fn with_output_piped(mut self, piped: bool) -> Self {
64        self.set_output_piped(piped);
65        self
66    }
67
68    /// Wrapper around [`alloc::str::replace`].
69    ///
70    /// This function is particularly useful when you need to replace
71    /// placeholders.
72    pub fn replace(mut self, from: impl AsRef<str>, to: impl AsRef<str>) -> Self {
73        self.inner = self.inner.replace(from.as_ref(), to.as_ref());
74        self
75    }
76
77    /// Runs the current command without input.
78    ///
79    /// See [`Command::run_with`] to run command with output.
80    pub async fn run(&self) -> Result<Output> {
81        self.run_with([]).await
82    }
83
84    /// Run the command with the given input.
85    ///
86    /// If the given input is empty, the command returns straight the
87    /// output. Otherwise the commands pipes this input to the
88    /// standard input channel then waits for the output on the
89    /// standard output channel.
90    pub async fn run_with(&self, input: impl AsRef<[u8]>) -> Result<Output> {
91        info!(cmd = self.inner, "run shell command");
92
93        let input = input.as_ref();
94
95        let stdin = if input.is_empty() {
96            debug!("inherit stdin from parent");
97            Stdio::inherit()
98        } else {
99            debug!("stdin piped");
100            Stdio::piped()
101        };
102
103        let mut cmd = new_async_command()
104            .arg(&self.inner)
105            .stdin(stdin)
106            .stdout(if self.piped {
107                debug!("stdout piped");
108                Stdio::piped()
109            } else {
110                debug!("inherit stdout from parent");
111                Stdio::inherit()
112            })
113            .stderr(if self.piped {
114                debug!("stderr piped");
115                Stdio::piped()
116            } else {
117                debug!("inherit stderr from parent");
118                Stdio::inherit()
119            })
120            .spawn()?;
121
122        if !input.is_empty() {
123            cmd.stdin
124                .as_mut()
125                .ok_or(Error::GetStdinError)?
126                .write_all(input)
127                .await?;
128        }
129
130        #[cfg(feature = "async-std")]
131        let output = cmd.output().await?;
132        #[cfg(feature = "tokio")]
133        let output = cmd.wait_with_output().await?;
134
135        let code = output
136            .status
137            .code()
138            .ok_or_else(|| Error::GetExitStatusCodeNotAvailableError(self.to_string()))?;
139
140        if code == 0 {
141            debug!(code, "shell command gracefully exited");
142        } else {
143            let cmd = self.to_string();
144            let err = String::from_utf8_lossy(&output.stderr).to_string();
145            debug!(code, err, "shell command ungracefully exited");
146            return Err(Error::GetExitStatusCodeNonZeroError(cmd, code, err));
147        }
148
149        Ok(Output::from(output.stdout))
150    }
151}
152
153impl Deref for Command {
154    type Target = String;
155
156    fn deref(&self) -> &Self::Target {
157        &self.inner
158    }
159}
160
161impl DerefMut for Command {
162    fn deref_mut(&mut self) -> &mut Self::Target {
163        &mut self.inner
164    }
165}
166
167impl From<String> for Command {
168    fn from(cmd: String) -> Self {
169        Self::new(cmd)
170    }
171}
172
173impl From<Command> for String {
174    fn from(cmd: Command) -> Self {
175        cmd.inner
176    }
177}
178
179impl ToString for Command {
180    fn to_string(&self) -> String {
181        self.inner.clone()
182    }
183}
184
185/// Prepares a new async command.
186fn new_async_command() -> AsyncCommand {
187    #[cfg(not(windows))]
188    let windows = false;
189    #[cfg(windows)]
190    let windows = !std::env::var("MSYSTEM")
191        .map(|env| env.starts_with("MINGW"))
192        .unwrap_or_default();
193
194    let (shell, arg) = if windows { ("cmd", "/C") } else { ("sh", "-c") };
195
196    let mut cmd = AsyncCommand::new(shell);
197    cmd.arg(arg);
198    cmd
199}