process/
lib.rs

1//! Rust library to run cross-platform, asynchronous processes in
2//! pipelines.
3//!
4//! The core concept of this library is to simplify the execution of
5//! commands, following these rules:
6//!
7//! 1. Commands are executed asynchronously, using the [tokio] async
8//! runtime.
9//!
10//! 2. Commands works on all major platforms (windows, macos and
11//! linux).
12//!
13//! 3. Commands can be executed in a pipeline, which means the output
14//! of the previous command is send as input of the next one.
15
16use log::{debug, error};
17use std::{
18    env, io,
19    ops::{Deref, DerefMut},
20    process::Stdio,
21    result,
22    string::FromUtf8Error,
23};
24use thiserror::Error;
25use tokio::{
26    io::{AsyncReadExt, AsyncWriteExt},
27    process::Command,
28};
29
30/// The global `Error` enum of the library.
31#[derive(Debug, Error)]
32pub enum Error {
33    #[error("cannot run command: {1}")]
34    SpawnProcessError(#[source] io::Error, String),
35    #[error("cannot get standard input")]
36    GetStdinError,
37    #[error("cannot wait for exit status code of command: {1}")]
38    WaitForExitStatusCodeError(#[source] io::Error, String),
39    #[error("cannot get exit status code of command: {0}")]
40    GetExitStatusCodeNotAvailableError(String),
41    #[error("command {0} returned non-zero exit status code {1}: {2}")]
42    InvalidExitStatusCodeNonZeroError(String, i32, String),
43    #[error("cannot write data to standard input")]
44    WriteStdinError(#[source] io::Error),
45    #[error("cannot get standard output")]
46    GetStdoutError,
47    #[error("cannot read data from standard output")]
48    ReadStdoutError(#[source] io::Error),
49    #[error("cannot get standard error")]
50    GetStderrError,
51    #[error("cannot read data from standard error")]
52    ReadStderrError(#[source] io::Error),
53    #[error("cannot get command output")]
54    GetOutputError(#[source] io::Error),
55    #[error("cannot parse command output as string")]
56    ParseOutputAsUtf8StringError(#[source] FromUtf8Error),
57}
58
59/// The global `Result` alias of the library.
60pub type Result<T> = result::Result<T, Error>;
61
62/// The main command structure.
63///
64/// A command can be either a single command or a pipeline.
65#[derive(Clone, Debug, Eq, PartialEq)]
66pub enum Cmd {
67    /// The single command variant.
68    SingleCmd(SingleCmd),
69
70    /// The pipeline variant.
71    Pipeline(Pipeline),
72}
73
74impl Cmd {
75    /// Wrapper around `alloc::str::replace`.
76    ///
77    /// This function is particularly useful when you need to replace
78    /// placeholders on all inner commands.
79    pub fn replace(mut self, from: impl AsRef<str>, to: impl AsRef<str>) -> Self {
80        match &mut self {
81            Self::SingleCmd(SingleCmd(cmd)) => *cmd = cmd.replace(from.as_ref(), to.as_ref()),
82            Self::Pipeline(Pipeline(cmds)) => {
83                for SingleCmd(cmd) in cmds {
84                    *cmd = cmd.replace(from.as_ref(), to.as_ref());
85                }
86            }
87        }
88        self
89    }
90
91    /// Runs the command with the given input.
92    pub async fn run_with(&self, input: impl AsRef<[u8]>) -> Result<CmdOutput> {
93        debug!("running command: {}", self.to_string());
94
95        match self {
96            Self::SingleCmd(cmd) => cmd.run(input).await,
97            Self::Pipeline(cmds) => cmds.run(input).await,
98        }
99    }
100
101    /// Runs the command without initial input.
102    pub async fn run(&self) -> Result<CmdOutput> {
103        self.run_with([]).await
104    }
105}
106
107impl Default for Cmd {
108    fn default() -> Self {
109        Self::Pipeline(Pipeline::default())
110    }
111}
112
113impl From<String> for Cmd {
114    fn from(cmd: String) -> Self {
115        Self::SingleCmd(cmd.into())
116    }
117}
118
119impl From<&String> for Cmd {
120    fn from(cmd: &String) -> Self {
121        Self::SingleCmd(cmd.into())
122    }
123}
124
125impl From<&str> for Cmd {
126    fn from(cmd: &str) -> Self {
127        Self::SingleCmd(cmd.into())
128    }
129}
130
131impl From<Vec<String>> for Cmd {
132    fn from(cmd: Vec<String>) -> Self {
133        Self::Pipeline(cmd.into())
134    }
135}
136
137impl From<Vec<&String>> for Cmd {
138    fn from(cmd: Vec<&String>) -> Self {
139        Self::Pipeline(cmd.into())
140    }
141}
142
143impl From<Vec<&str>> for Cmd {
144    fn from(cmd: Vec<&str>) -> Self {
145        Self::Pipeline(cmd.into())
146    }
147}
148
149impl From<&[String]> for Cmd {
150    fn from(cmd: &[String]) -> Self {
151        Self::Pipeline(cmd.into())
152    }
153}
154
155impl From<&[&String]> for Cmd {
156    fn from(cmd: &[&String]) -> Self {
157        Self::Pipeline(cmd.into())
158    }
159}
160
161impl From<&[&str]> for Cmd {
162    fn from(cmd: &[&str]) -> Self {
163        Self::Pipeline(cmd.into())
164    }
165}
166
167impl ToString for Cmd {
168    fn to_string(&self) -> String {
169        match self {
170            Self::SingleCmd(cmd) => cmd.to_string(),
171            Self::Pipeline(pipeline) => pipeline.to_string(),
172        }
173    }
174}
175
176/// The single command structure.
177///
178/// Represents commands that are only composed of one single command.
179#[derive(Clone, Debug, Eq, PartialEq)]
180pub struct SingleCmd(String);
181
182impl SingleCmd {
183    /// Runs the single command with the given input.
184    ///
185    /// If the given input is empty, the command gets straight the
186    /// output. Otherwise the commands pipes this input to the
187    /// standard input channel then waits for the output on the
188    /// standard output channel.
189    async fn run(&self, input: impl AsRef<[u8]>) -> Result<CmdOutput> {
190        let windows = cfg!(target_os = "windows")
191            && !(env::var("MSYSTEM")
192                .map(|env| env.starts_with("MINGW"))
193                .unwrap_or_default());
194
195        let (shell, arg) = if windows { ("cmd", "/C") } else { ("sh", "-c") };
196        let mut cmd = Command::new(shell);
197        let cmd = cmd.args(&[arg, &self.0]);
198
199        if input.as_ref().is_empty() {
200            let output = cmd.output().await.map_err(Error::GetOutputError)?;
201            let code = output
202                .status
203                .code()
204                .ok_or_else(|| Error::GetExitStatusCodeNotAvailableError(self.to_string()))?;
205
206            if code != 0 {
207                let cmd = self.to_string();
208                let err = String::from_utf8_lossy(&output.stderr).to_string();
209                return Err(Error::InvalidExitStatusCodeNonZeroError(cmd, code, err));
210            }
211
212            Ok(output.stdout.into())
213        } else {
214            let mut output = Vec::new();
215
216            let mut pipeline = cmd
217                .stdin(Stdio::piped())
218                .stdout(Stdio::piped())
219                .stderr(Stdio::piped())
220                .spawn()
221                .map_err(|err| Error::SpawnProcessError(err, self.to_string()))?;
222
223            pipeline
224                .stdin
225                .as_mut()
226                .ok_or(Error::GetStdinError)?
227                .write_all(input.as_ref())
228                .await
229                .map_err(Error::WriteStdinError)?;
230
231            let code = pipeline
232                .wait()
233                .await
234                .map_err(|err| Error::WaitForExitStatusCodeError(err, self.to_string()))?
235                .code()
236                .ok_or_else(|| Error::GetExitStatusCodeNotAvailableError(self.to_string()))?;
237
238            if code != 0 {
239                let cmd = self.to_string();
240                let mut err = Vec::new();
241                pipeline
242                    .stderr
243                    .as_mut()
244                    .ok_or(Error::GetStderrError)?
245                    .read_to_end(&mut err)
246                    .await
247                    .map_err(Error::ReadStderrError)?;
248                let err = String::from_utf8_lossy(&err).to_string();
249
250                return Err(Error::InvalidExitStatusCodeNonZeroError(cmd, code, err));
251            }
252
253            pipeline
254                .stdout
255                .as_mut()
256                .ok_or(Error::GetStdoutError)?
257                .read_to_end(&mut output)
258                .await
259                .map_err(Error::ReadStdoutError)?;
260
261            Ok(output.into())
262        }
263    }
264}
265
266impl Deref for SingleCmd {
267    type Target = String;
268
269    fn deref(&self) -> &Self::Target {
270        &self.0
271    }
272}
273
274impl DerefMut for SingleCmd {
275    fn deref_mut(&mut self) -> &mut Self::Target {
276        &mut self.0
277    }
278}
279
280impl From<String> for SingleCmd {
281    fn from(cmd: String) -> Self {
282        Self(cmd)
283    }
284}
285
286impl From<&String> for SingleCmd {
287    fn from(cmd: &String) -> Self {
288        Self(cmd.clone())
289    }
290}
291
292impl From<&str> for SingleCmd {
293    fn from(cmd: &str) -> Self {
294        Self(cmd.to_owned())
295    }
296}
297
298impl ToString for SingleCmd {
299    fn to_string(&self) -> String {
300        self.0.clone()
301    }
302}
303
304/// The command pipeline structure.
305///
306/// Represents commands that are composed of multiple single
307/// commands. Commands are run in a pipeline, which means the output
308/// of the previous command is piped to the input of the next one, and
309/// so on.
310#[derive(Clone, Debug, Default, Eq, PartialEq)]
311pub struct Pipeline(Vec<SingleCmd>);
312
313impl Pipeline {
314    /// Runs the command pipeline with the given input.
315    async fn run(&self, input: impl AsRef<[u8]>) -> Result<CmdOutput> {
316        let mut output = input.as_ref().to_owned();
317
318        for cmd in &self.0 {
319            output = cmd.run(&output).await?.0;
320        }
321
322        Ok(output.into())
323    }
324}
325
326impl Deref for Pipeline {
327    type Target = Vec<SingleCmd>;
328
329    fn deref(&self) -> &Self::Target {
330        &self.0
331    }
332}
333
334impl DerefMut for Pipeline {
335    fn deref_mut(&mut self) -> &mut Self::Target {
336        &mut self.0
337    }
338}
339
340impl From<Vec<String>> for Pipeline {
341    fn from(cmd: Vec<String>) -> Self {
342        Self(cmd.into_iter().map(Into::into).collect())
343    }
344}
345
346impl From<Vec<&String>> for Pipeline {
347    fn from(cmd: Vec<&String>) -> Self {
348        Self(cmd.into_iter().map(Into::into).collect())
349    }
350}
351
352impl From<Vec<&str>> for Pipeline {
353    fn from(cmd: Vec<&str>) -> Self {
354        Self(cmd.into_iter().map(Into::into).collect())
355    }
356}
357
358impl From<&[String]> for Pipeline {
359    fn from(cmd: &[String]) -> Self {
360        Self(cmd.iter().map(Into::into).collect())
361    }
362}
363
364impl From<&[&String]> for Pipeline {
365    fn from(cmd: &[&String]) -> Self {
366        Self(cmd.iter().map(|cmd| (*cmd).into()).collect())
367    }
368}
369
370impl From<&[&str]> for Pipeline {
371    fn from(cmd: &[&str]) -> Self {
372        Self(cmd.iter().map(|cmd| (*cmd).into()).collect())
373    }
374}
375
376impl ToString for Pipeline {
377    fn to_string(&self) -> String {
378        self.0.iter().fold(String::new(), |s, cmd| {
379            if s.is_empty() {
380                cmd.to_string()
381            } else {
382                s + "|" + &cmd.to_string()
383            }
384        })
385    }
386}
387
388/// Wrapper around command output.
389///
390/// The only role of this struct is to provide convenient functions to
391/// export command output as string.
392#[derive(Clone, Debug, Default, Eq, PartialEq)]
393pub struct CmdOutput(Vec<u8>);
394
395impl CmdOutput {
396    /// Reads the command output as string lossy.
397    pub fn to_string_lossy(&self) -> String {
398        String::from_utf8_lossy(self).to_string()
399    }
400}
401
402impl Deref for CmdOutput {
403    type Target = Vec<u8>;
404
405    fn deref(&self) -> &Self::Target {
406        &self.0
407    }
408}
409
410impl DerefMut for CmdOutput {
411    fn deref_mut(&mut self) -> &mut Self::Target {
412        &mut self.0
413    }
414}
415
416impl From<Vec<u8>> for CmdOutput {
417    fn from(output: Vec<u8>) -> Self {
418        Self(output)
419    }
420}
421
422impl Into<Vec<u8>> for CmdOutput {
423    fn into(self) -> Vec<u8> {
424        self.0
425    }
426}
427
428impl TryInto<String> for CmdOutput {
429    type Error = Error;
430
431    fn try_into(self) -> result::Result<String, Self::Error> {
432        String::from_utf8(self.0).map_err(Error::ParseOutputAsUtf8StringError)
433    }
434}