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