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}