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}