Skip to main content

qubit_command/
command.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9use std::{
10    ffi::{
11        OsStr,
12        OsString,
13    },
14    path::{
15        Path,
16        PathBuf,
17    },
18};
19
20use crate::command_env::env_key_eq;
21use crate::command_stdin::CommandStdin;
22
23/// Structured description of an external command to run.
24///
25/// `Command` stores a program and argument vector instead of parsing a
26/// shell-like command line. This avoids quoting ambiguity and accidental shell
27/// injection. Use [`Self::shell`] only when shell parsing, redirection,
28/// expansion, or pipes are intentionally required.
29///
30/// # Author
31///
32/// Haixing Hu
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct Command {
35    /// Program executable name or path.
36    program: OsString,
37    /// Positional arguments passed to the program.
38    args: Vec<OsString>,
39    /// Working directory override for this command.
40    working_directory: Option<PathBuf>,
41    /// Whether the command should clear inherited environment variables.
42    clear_environment: bool,
43    /// Environment variables added or overridden for this command.
44    envs: Vec<(OsString, OsString)>,
45    /// Environment variables removed for this command.
46    removed_envs: Vec<OsString>,
47    /// Standard input configuration for this command.
48    stdin: CommandStdin,
49}
50
51impl Command {
52    /// Creates a command from a program name or path.
53    ///
54    /// # Parameters
55    ///
56    /// * `program` - Executable name or path to run.
57    ///
58    /// # Returns
59    ///
60    /// A command with no arguments or per-command overrides.
61    #[inline]
62    pub fn new(program: &str) -> Self {
63        Self::new_os(program)
64    }
65
66    /// Creates a command from a program name or path that may not be UTF-8.
67    ///
68    /// # Parameters
69    ///
70    /// * `program` - Executable name or path to run.
71    ///
72    /// # Returns
73    ///
74    /// A command with no arguments or per-command overrides.
75    #[inline]
76    pub fn new_os<S>(program: S) -> Self
77    where
78        S: AsRef<OsStr>,
79    {
80        Self {
81            program: program.as_ref().to_owned(),
82            args: Vec::new(),
83            working_directory: None,
84            clear_environment: false,
85            envs: Vec::new(),
86            removed_envs: Vec::new(),
87            stdin: CommandStdin::Null,
88        }
89    }
90
91    /// Creates a command executed through the platform shell.
92    ///
93    /// On Unix-like platforms this creates `sh -c <command_line>`. On Windows
94    /// this creates `cmd /C <command_line>`. Prefer [`Self::new`] with explicit
95    /// arguments when shell parsing is not required.
96    ///
97    /// # Parameters
98    ///
99    /// * `command_line` - Shell command line to execute.
100    ///
101    /// # Returns
102    ///
103    /// A command that invokes the platform shell.
104    #[cfg(not(windows))]
105    #[inline]
106    pub fn shell(command_line: &str) -> Self {
107        Self::new("sh").arg("-c").arg(command_line)
108    }
109
110    /// Creates a command executed through the platform shell.
111    ///
112    /// On Windows this creates `cmd /C <command_line>`. Prefer [`Self::new`]
113    /// with explicit arguments when shell parsing is not required.
114    ///
115    /// # Parameters
116    ///
117    /// * `command_line` - Shell command line to execute.
118    ///
119    /// # Returns
120    ///
121    /// A command that invokes the platform shell.
122    #[cfg(windows)]
123    #[inline]
124    pub fn shell(command_line: &str) -> Self {
125        Self::new("cmd").arg("/C").arg(command_line)
126    }
127
128    /// Adds one positional argument.
129    ///
130    /// # Parameters
131    ///
132    /// * `arg` - Argument to append.
133    ///
134    /// # Returns
135    ///
136    /// The updated command.
137    #[inline]
138    pub fn arg(mut self, arg: &str) -> Self {
139        self.args.push(OsString::from(arg));
140        self
141    }
142
143    /// Adds one positional argument that may not be UTF-8.
144    ///
145    /// # Parameters
146    ///
147    /// * `arg` - Argument to append.
148    ///
149    /// # Returns
150    ///
151    /// The updated command.
152    #[inline]
153    pub fn arg_os<S>(mut self, arg: S) -> Self
154    where
155        S: AsRef<OsStr>,
156    {
157        self.args.push(arg.as_ref().to_owned());
158        self
159    }
160
161    /// Adds multiple positional arguments.
162    ///
163    /// # Parameters
164    ///
165    /// * `args` - Arguments to append in order.
166    ///
167    /// # Returns
168    ///
169    /// The updated command.
170    #[inline]
171    pub fn args(mut self, args: &[&str]) -> Self {
172        self.args.extend(args.iter().map(OsString::from));
173        self
174    }
175
176    /// Adds multiple positional arguments that may not be UTF-8.
177    ///
178    /// # Parameters
179    ///
180    /// * `args` - Arguments to append in order.
181    ///
182    /// # Returns
183    ///
184    /// The updated command.
185    pub fn args_os<I, S>(mut self, args: I) -> Self
186    where
187        I: IntoIterator<Item = S>,
188        S: AsRef<OsStr>,
189    {
190        self.args
191            .extend(args.into_iter().map(|arg| arg.as_ref().to_owned()));
192        self
193    }
194
195    /// Sets a per-command working directory.
196    ///
197    /// # Parameters
198    ///
199    /// * `working_directory` - Directory used as the child process working
200    ///   directory.
201    ///
202    /// # Returns
203    ///
204    /// The updated command.
205    #[inline]
206    pub fn working_directory<P>(mut self, working_directory: P) -> Self
207    where
208        P: Into<PathBuf>,
209    {
210        self.working_directory = Some(working_directory.into());
211        self
212    }
213
214    /// Adds or overrides an environment variable for this command.
215    ///
216    /// # Parameters
217    ///
218    /// * `key` - Environment variable name.
219    /// * `value` - Environment variable value.
220    ///
221    /// # Returns
222    ///
223    /// The updated command.
224    #[inline]
225    pub fn env(mut self, key: &str, value: &str) -> Self {
226        self = self.env_os(key, value);
227        self
228    }
229
230    /// Adds or overrides an environment variable that may not be UTF-8.
231    ///
232    /// # Parameters
233    ///
234    /// * `key` - Environment variable name.
235    /// * `value` - Environment variable value.
236    ///
237    /// # Returns
238    ///
239    /// The updated command.
240    pub fn env_os<K, V>(mut self, key: K, value: V) -> Self
241    where
242        K: AsRef<OsStr>,
243        V: AsRef<OsStr>,
244    {
245        let key = key.as_ref().to_owned();
246        let value = value.as_ref().to_owned();
247        self.removed_envs
248            .retain(|removed| !env_key_eq(removed, &key));
249        self.envs
250            .retain(|(existing_key, _)| !env_key_eq(existing_key, &key));
251        self.envs.push((key, value));
252        self
253    }
254
255    /// Removes an inherited or previously configured environment variable.
256    ///
257    /// # Parameters
258    ///
259    /// * `key` - Environment variable name to remove.
260    ///
261    /// # Returns
262    ///
263    /// The updated command.
264    #[inline]
265    pub fn env_remove(mut self, key: &str) -> Self {
266        self = self.env_remove_os(key);
267        self
268    }
269
270    /// Removes an environment variable whose name may not be UTF-8.
271    ///
272    /// # Parameters
273    ///
274    /// * `key` - Environment variable name to remove.
275    ///
276    /// # Returns
277    ///
278    /// The updated command.
279    pub fn env_remove_os<S>(mut self, key: S) -> Self
280    where
281        S: AsRef<OsStr>,
282    {
283        let key = key.as_ref().to_owned();
284        self.envs
285            .retain(|(existing_key, _)| !env_key_eq(existing_key, &key));
286        self.removed_envs
287            .retain(|removed| !env_key_eq(removed, &key));
288        self.removed_envs.push(key);
289        self
290    }
291
292    /// Clears all inherited environment variables for this command.
293    ///
294    /// Environment variables added after this call are still passed to the child
295    /// process.
296    ///
297    /// # Returns
298    ///
299    /// The updated command.
300    pub fn env_clear(mut self) -> Self {
301        self.clear_environment = true;
302        self.envs.clear();
303        self.removed_envs.clear();
304        self
305    }
306
307    /// Connects the command stdin to null input.
308    ///
309    /// # Returns
310    ///
311    /// The updated command.
312    pub fn stdin_null(mut self) -> Self {
313        self.stdin = CommandStdin::Null;
314        self
315    }
316
317    /// Inherits stdin from the parent process.
318    ///
319    /// # Returns
320    ///
321    /// The updated command.
322    pub fn stdin_inherit(mut self) -> Self {
323        self.stdin = CommandStdin::Inherit;
324        self
325    }
326
327    /// Writes bytes to the child process stdin.
328    ///
329    /// The runner writes the bytes on a helper thread after spawning the child
330    /// process, then closes stdin so the child can observe EOF.
331    ///
332    /// # Parameters
333    ///
334    /// * `bytes` - Bytes to send to stdin.
335    ///
336    /// # Returns
337    ///
338    /// The updated command.
339    pub fn stdin_bytes<B>(mut self, bytes: B) -> Self
340    where
341        B: Into<Vec<u8>>,
342    {
343        self.stdin = CommandStdin::Bytes(bytes.into());
344        self
345    }
346
347    /// Reads child process stdin from a file.
348    ///
349    /// # Parameters
350    ///
351    /// * `path` - File path to open and connect to stdin.
352    ///
353    /// # Returns
354    ///
355    /// The updated command.
356    pub fn stdin_file<P>(mut self, path: P) -> Self
357    where
358        P: Into<PathBuf>,
359    {
360        self.stdin = CommandStdin::File(path.into());
361        self
362    }
363
364    /// Returns the executable name or path.
365    ///
366    /// # Returns
367    ///
368    /// Program executable name or path as an [`OsStr`].
369    #[inline]
370    pub fn program(&self) -> &OsStr {
371        &self.program
372    }
373
374    /// Returns the configured argument list.
375    ///
376    /// # Returns
377    ///
378    /// Borrowed argument list in submission order.
379    #[inline]
380    pub fn arguments(&self) -> &[OsString] {
381        &self.args
382    }
383
384    /// Returns the per-command working directory override.
385    ///
386    /// # Returns
387    ///
388    /// `Some(path)` when the command has a working directory override, or
389    /// `None` when the runner default should be used.
390    #[inline]
391    pub fn working_directory_override(&self) -> Option<&Path> {
392        self.working_directory.as_deref()
393    }
394
395    /// Returns environment variable overrides.
396    ///
397    /// # Returns
398    ///
399    /// Borrowed environment variable entries in insertion order.
400    #[inline]
401    pub fn environment(&self) -> &[(OsString, OsString)] {
402        &self.envs
403    }
404
405    /// Returns environment variable removals.
406    ///
407    /// # Returns
408    ///
409    /// Borrowed environment variable names removed before spawning the command.
410    #[inline]
411    pub fn removed_environment(&self) -> &[OsString] {
412        &self.removed_envs
413    }
414
415    /// Returns whether the inherited environment is cleared.
416    ///
417    /// # Returns
418    ///
419    /// `true` when the command should start from an empty environment.
420    #[inline]
421    pub const fn clears_environment(&self) -> bool {
422        self.clear_environment
423    }
424
425    /// Consumes the command and returns the configured stdin behavior.
426    ///
427    /// # Returns
428    ///
429    /// Owned stdin configuration used by the runner.
430    #[inline]
431    pub(crate) fn into_stdin_configuration(self) -> CommandStdin {
432        self.stdin
433    }
434
435    /// Formats this command for diagnostics.
436    ///
437    /// # Returns
438    ///
439    /// An argv-style command string suitable for logs and errors.
440    pub(crate) fn display_command(&self) -> String {
441        let mut parts = Vec::with_capacity(self.args.len() + 1);
442        parts.push(self.program.as_os_str());
443        for arg in &self.args {
444            parts.push(arg.as_os_str());
445        }
446        format!("{parts:?}")
447    }
448}