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
20/// Structured description of an external command to run.
21///
22/// `Command` stores a program and argument vector instead of parsing a
23/// shell-like command line. This avoids quoting ambiguity and accidental shell
24/// injection. Use [`Self::shell`] only when shell parsing, redirection,
25/// expansion, or pipes are intentionally required.
26///
27/// # Author
28///
29/// Haixing Hu
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct Command {
32    /// Program executable name or path.
33    program: OsString,
34    /// Positional arguments passed to the program.
35    args: Vec<OsString>,
36    /// Working directory override for this command.
37    working_directory: Option<PathBuf>,
38    /// Environment variables added or overridden for this command.
39    envs: Vec<(OsString, OsString)>,
40}
41
42impl Command {
43    /// Creates a command from a program name or path.
44    ///
45    /// # Parameters
46    ///
47    /// * `program` - Executable name or path to run.
48    ///
49    /// # Returns
50    ///
51    /// A command with no arguments or per-command overrides.
52    #[inline]
53    pub fn new(program: &str) -> Self {
54        Self {
55            program: OsString::from(program),
56            args: Vec::new(),
57            working_directory: None,
58            envs: Vec::new(),
59        }
60    }
61
62    /// Creates a command executed through the platform shell.
63    ///
64    /// On Unix-like platforms this creates `sh -c <command_line>`. On Windows
65    /// this creates `cmd /C <command_line>`. Prefer [`Self::new`] with explicit
66    /// arguments when shell parsing is not required.
67    ///
68    /// # Parameters
69    ///
70    /// * `command_line` - Shell command line to execute.
71    ///
72    /// # Returns
73    ///
74    /// A command that invokes the platform shell.
75    #[cfg(not(windows))]
76    #[inline]
77    pub fn shell(command_line: &str) -> Self {
78        Self::new("sh").arg("-c").arg(command_line)
79    }
80
81    /// Creates a command executed through the platform shell.
82    ///
83    /// On Windows this creates `cmd /C <command_line>`. Prefer [`Self::new`]
84    /// with explicit arguments when shell parsing is not required.
85    ///
86    /// # Parameters
87    ///
88    /// * `command_line` - Shell command line to execute.
89    ///
90    /// # Returns
91    ///
92    /// A command that invokes the platform shell.
93    #[cfg(windows)]
94    #[inline]
95    pub fn shell(command_line: &str) -> Self {
96        Self::new("cmd").arg("/C").arg(command_line)
97    }
98
99    /// Adds one positional argument.
100    ///
101    /// # Parameters
102    ///
103    /// * `arg` - Argument to append.
104    ///
105    /// # Returns
106    ///
107    /// The updated command.
108    #[inline]
109    pub fn arg(mut self, arg: &str) -> Self {
110        self.args.push(OsString::from(arg));
111        self
112    }
113
114    /// Adds multiple positional arguments.
115    ///
116    /// # Parameters
117    ///
118    /// * `args` - Arguments to append in order.
119    ///
120    /// # Returns
121    ///
122    /// The updated command.
123    #[inline]
124    pub fn args(mut self, args: &[&str]) -> Self {
125        self.args.extend(args.iter().map(OsString::from));
126        self
127    }
128
129    /// Sets a per-command working directory.
130    ///
131    /// # Parameters
132    ///
133    /// * `working_directory` - Directory used as the child process working
134    ///   directory.
135    ///
136    /// # Returns
137    ///
138    /// The updated command.
139    #[inline]
140    pub fn working_directory<P>(mut self, working_directory: P) -> Self
141    where
142        P: Into<PathBuf>,
143    {
144        self.working_directory = Some(working_directory.into());
145        self
146    }
147
148    /// Adds or overrides an environment variable for this command.
149    ///
150    /// # Parameters
151    ///
152    /// * `key` - Environment variable name.
153    /// * `value` - Environment variable value.
154    ///
155    /// # Returns
156    ///
157    /// The updated command.
158    #[inline]
159    pub fn env(mut self, key: &str, value: &str) -> Self {
160        self.envs.push((OsString::from(key), OsString::from(value)));
161        self
162    }
163
164    /// Returns the executable name or path.
165    ///
166    /// # Returns
167    ///
168    /// Program executable name or path as an [`OsStr`].
169    #[inline]
170    pub fn program(&self) -> &OsStr {
171        &self.program
172    }
173
174    /// Returns the configured argument list.
175    ///
176    /// # Returns
177    ///
178    /// Borrowed argument list in submission order.
179    #[inline]
180    pub fn arguments(&self) -> &[OsString] {
181        &self.args
182    }
183
184    /// Returns the per-command working directory override.
185    ///
186    /// # Returns
187    ///
188    /// `Some(path)` when the command has a working directory override, or
189    /// `None` when the runner default should be used.
190    #[inline]
191    pub fn working_directory_override(&self) -> Option<&Path> {
192        self.working_directory.as_deref()
193    }
194
195    /// Returns environment variable overrides.
196    ///
197    /// # Returns
198    ///
199    /// Borrowed environment variable entries in insertion order.
200    #[inline]
201    pub fn environment(&self) -> &[(OsString, OsString)] {
202        &self.envs
203    }
204
205    /// Formats this command for diagnostics.
206    ///
207    /// # Returns
208    ///
209    /// A lossy, human-readable command string suitable for logs and errors.
210    pub(crate) fn display_command(&self) -> String {
211        let mut text = self.program.to_string_lossy().into_owned();
212        for arg in &self.args {
213            text.push(' ');
214            text.push_str(&arg.to_string_lossy());
215        }
216        text
217    }
218}