io_process/
command.rs

1//! Module dedicated to the [`Command`] builder.
2
3use std::{borrow::Cow, collections::HashMap, fmt, path::PathBuf, process::Stdio};
4
5/// The command builder.
6///
7/// The aim of this builder is to be able to declare a command using
8/// the same API from [`std::process::Command`], without any I/O
9/// interaction. I/O connectors can then take data from this builder
10/// to build I/O-specific commands.
11///
12/// Refs: [`std::process::Command`]
13#[derive(Default)]
14pub struct Command {
15    /// Path to the program.
16    ///
17    /// Refs: [`std::process::Command::get_program`]
18    program: String,
19
20    /// Arguments that will be passed to the program.
21    ///
22    /// Refs: [`std::process::Command::get_args`]
23    args: Option<Vec<String>>,
24
25    /// Environment variables explicitly set for the child process.
26    ///
27    /// Refs: [`std::process::Command::get_envs`]
28    pub envs: Option<HashMap<String, String>>,
29
30    /// Working directory of the child process.
31    ///
32    /// Refs: [`std::process::Command::get_current_dir`]
33    pub current_dir: Option<PathBuf>,
34
35    /// Configuration for the child process's standard input (stdin)
36    /// handle.
37    ///
38    /// Refs: [`std::process::Command::stdin`]
39    pub stdin: Option<Stdio>,
40
41    /// Configuration for the child process's standard output (stdout)
42    /// handle.
43    ///
44    /// Refs: [`std::process::Command::stdout`]
45    pub stdout: Option<Stdio>,
46
47    /// Configuration for the child process's standard error (stderr)
48    /// handle.
49    ///
50    /// Refs: [`std::process::Command::stderr`]
51    pub stderr: Option<Stdio>,
52
53    /// Should shell-expand program and arguments.
54    ///
55    /// When true, tilde `~` and environment variables `$ENV` are
56    /// expanded for the program and arguments only.
57    ///
58    /// Requires the `expand` cargo feature.
59    #[cfg(feature = "expand")]
60    pub expand: bool,
61}
62
63impl fmt::Debug for Command {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        let mut debug = f.debug_struct("Command");
66
67        debug.field("program", &self.program);
68
69        if let Some(args) = &self.args {
70            debug.field("args", args);
71        }
72
73        if let Some(envs) = &self.envs {
74            debug.field("envs", &envs.keys());
75        }
76
77        if let Some(dir) = &self.current_dir {
78            debug.field("current_dir", &dir);
79        }
80
81        if let Some(stdin) = &self.stdin {
82            debug.field("stdin", &stdin);
83        }
84
85        if let Some(stdout) = &self.stdout {
86            debug.field("stdout", &stdout);
87        }
88
89        if let Some(stderr) = &self.stderr {
90            debug.field("stderr", &stderr);
91        }
92
93        #[cfg(feature = "expand")]
94        debug.field("expand", &self.expand);
95
96        debug.finish()
97    }
98}
99
100impl Command {
101    /// Constructs a new [`Command`] for launching the program at path
102    /// `program`. This is just a builder, it does not launch any
103    /// program on its own. Only I/O connectors do spawn processes.
104    ///
105    /// Refs: [`std::process::Command::new`]
106    pub fn new<S: ToString>(program: S) -> Self {
107        Self {
108            program: program.to_string(),
109            args: None,
110            envs: None,
111            current_dir: None,
112            stdin: None,
113            stdout: None,
114            stderr: None,
115            #[cfg(feature = "expand")]
116            expand: false,
117        }
118    }
119
120    #[cfg(feature = "expand")]
121    pub fn expand<'a>(&self, input: &'a str) -> Cow<'a, str> {
122        let home_dir = || dirs::home_dir().map(|p| p.to_string_lossy().to_string());
123        let get_env = |key: &str| -> Result<Option<Cow<str>>, ()> {
124            if let Some(envs) = &self.envs {
125                if let Some(val) = envs.get(key) {
126                    return Ok(Some(val.into()));
127                }
128            }
129
130            match std::env::var(key) {
131                Ok(val) => Ok(Some(val.into())),
132                Err(_) => Ok(None),
133            }
134        };
135
136        if let Ok(input) = shellexpand::full_with_context(input, home_dir, get_env) {
137            return input;
138        }
139
140        input.into()
141    }
142
143    /// Gets the program as str [`Cow`].
144    ///
145    /// If the `expand` cargo feature is enabled, and
146    /// [`Command::expand`] is true, then program is also
147    /// shell-expanded.
148    pub fn get_program(&self) -> Cow<'_, str> {
149        #[cfg(feature = "expand")]
150        if self.expand {
151            return self.expand(&self.program);
152        }
153
154        Cow::from(&self.program)
155    }
156
157    /// Adds an argument to pass to the program.
158    ///
159    /// Refs: [`std::process::Command::arg`]
160    pub fn arg<S: ToString>(&mut self, arg: S) -> &mut Self {
161        match &mut self.args {
162            Some(args) => {
163                args.push(arg.to_string());
164            }
165            None => {
166                self.args = Some(vec![arg.to_string()]);
167            }
168        }
169        self
170    }
171
172    /// Adds multiple arguments to pass to the program.
173    ///
174    /// Refs: [`std::process::Command::args`]
175    pub fn args<I, S>(&mut self, args: I) -> &mut Self
176    where
177        I: IntoIterator<Item = S>,
178        S: ToString,
179    {
180        for arg in args {
181            self.arg(arg);
182        }
183        self
184    }
185
186    /// Gets the arguments as str [`Cow`]s.
187    ///
188    /// If the `expand` cargo feature is enabled, and
189    /// [`Command::expand`] is true, then arguments are also
190    /// shell-expanded.
191    pub fn get_args(&self) -> Option<Vec<Cow<'_, str>>> {
192        let self_args = self.args.as_ref()?;
193        let mut args = Vec::with_capacity(self_args.len());
194
195        for self_arg in self_args {
196            #[cfg(feature = "expand")]
197            if self.expand {
198                args.push(self.expand(self_arg));
199                continue;
200            }
201
202            args.push(self_arg.into());
203        }
204
205        Some(args)
206    }
207
208    /// Inserts or updates an explicit environment variable mapping.
209    ///
210    /// Refs: [`std::process::Command::env`]
211    pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
212    where
213        K: ToString,
214        V: ToString,
215    {
216        let key = key.to_string();
217        let val = val.to_string();
218
219        match &mut self.envs {
220            Some(envs) => {
221                envs.insert(key, val);
222            }
223            None => {
224                self.envs = Some(HashMap::from_iter(Some((key, val))));
225            }
226        }
227        self
228    }
229
230    /// Inserts or updates multiple explicit environment variable
231    /// mappings.
232    ///
233    /// Refs: [`std::process::Command::envs`]
234    pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
235    where
236        I: IntoIterator<Item = (K, V)>,
237        K: ToString,
238        V: ToString,
239    {
240        for (key, val) in vars {
241            self.env(key, val);
242        }
243        self
244    }
245
246    /// Removes an explicitly set environment variable and prevents
247    /// inheriting it from a parent process.
248    ///
249    /// Refs: [`std::process::Command::env_remove`]
250    pub fn env_remove<K: AsRef<str>>(&mut self, key: K) -> &mut Self {
251        if let Some(envs) = &mut self.envs {
252            envs.remove(key.as_ref());
253        }
254        self
255    }
256
257    /// Clears all explicitly set environment variables and prevents
258    /// inheriting any parent process environment variables.
259    ///
260    /// Refs: [`std::process::Command::env_clear`]
261    pub fn env_clear(&mut self) -> &mut Self {
262        if let Some(envs) = &mut self.envs {
263            envs.clear();
264        }
265        self.envs = None;
266        self
267    }
268
269    /// Sets the working directory for the child process.
270    ///
271    /// Refs: [`std::process::Command::current_dir`]
272    pub fn current_dir<P: Into<PathBuf>>(&mut self, dir: P) -> &mut Self {
273        self.current_dir = Some(dir.into());
274        self
275    }
276
277    /// Configuration for the child process's standard input (stdin)
278    /// handle.
279    ///
280    /// Refs: [`std::process::Command::stdin`]
281    pub fn stdin<T: Into<Stdio>>(&mut self, cfg: T) -> &mut Command {
282        self.stdin = Some(cfg.into());
283        self
284    }
285
286    /// Configuration for the child process's standard output (stdout)
287    /// handle.
288    ///
289    /// Refs: [`std::process::Command::stdout`]
290    pub fn stdout<T: Into<Stdio>>(&mut self, cfg: T) -> &mut Command {
291        self.stdout = Some(cfg.into());
292        self
293    }
294
295    /// Configuration for the child process's standard error (stderr)
296    /// handle.
297    ///
298    /// Refs: [`std::process::Command::stderr`]
299    pub fn stderr<T: Into<Stdio>>(&mut self, cfg: T) -> &mut Command {
300        self.stderr = Some(cfg.into());
301        self
302    }
303}
304
305impl Clone for Command {
306    fn clone(&self) -> Self {
307        let mut command = Command::new(&self.program);
308
309        #[cfg(feature = "expand")]
310        {
311            command.expand = self.expand;
312        }
313
314        if let Some(args) = self.args.as_ref() {
315            for arg in args {
316                command.arg(arg);
317            }
318        }
319
320        if let Some(envs) = self.envs.as_ref() {
321            for (key, val) in envs {
322                command.env(key, val);
323            }
324        }
325
326        if let Some(dir) = self.current_dir.as_ref() {
327            command.current_dir(dir);
328        }
329
330        command
331    }
332}
333
334impl Eq for Command {}
335
336impl PartialEq for Command {
337    fn eq(&self, other: &Self) -> bool {
338        if self.program != other.program {
339            return false;
340        }
341
342        if self.args != other.args {
343            return false;
344        }
345
346        if self.current_dir != other.current_dir {
347            return false;
348        }
349
350        #[cfg(feature = "expand")]
351        if self.expand != other.expand {
352            return false;
353        }
354
355        true
356    }
357}