Skip to main content

qubit_command/
command.rs

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