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}