gix_command/prepare.rs
1use std::{
2 borrow::Cow,
3 ffi::{OsStr, OsString},
4 path::{Path, PathBuf},
5 process::{Command, Stdio},
6};
7
8use bstr::ByteSlice;
9
10use crate::{Context, Prepare, extract_interpreter, is_bare_command, split_paths, win_path_lookup};
11
12/// Builder
13impl Prepare {
14 /// If called, the command will be checked for characters that are typical for shell
15 /// scripts, and if found will use `sh` to execute it or whatever is set as
16 /// [`with_shell_program()`](Self::with_shell_program()).
17 ///
18 /// Commands are inspected as bytes, including non-UTF-8 commands on Unix. If the platform
19 /// cannot represent a command as bytes, it is invoked directly.
20 ///
21 /// If a shell is used, then arguments given here with [arg()](Self::arg) or
22 /// [args()](Self::args) will be substituted via `"$@"` if it's not already present in the
23 /// command.
24 ///
25 ///
26 /// The [`command_may_be_shell_script_allow_manual_argument_splitting()`](Self::command_may_be_shell_script_allow_manual_argument_splitting())
27 /// and [`command_may_be_shell_script_disallow_manual_argument_splitting()`](Self::command_may_be_shell_script_disallow_manual_argument_splitting())
28 /// methods also call this method.
29 ///
30 /// If neither this method nor [`with_shell()`](Self::with_shell()) is called, commands are
31 /// always executed verbatim and directly, without the use of a shell.
32 pub fn command_may_be_shell_script(mut self) -> Self {
33 self.use_shell = gix_path::os_str_into_bstr(&self.command)
34 .is_ok_and(|cmd| cmd.find_byteset(b"|&;<>()$`\\\"' \t\n*?[#~=%").is_some());
35 self
36 }
37
38 /// If called, unconditionally use a shell to execute the command and its arguments.
39 ///
40 /// This uses `sh` to execute it, or whatever is set as
41 /// [`with_shell_program()`](Self::with_shell_program()).
42 ///
43 /// Arguments given here with [arg()](Self::arg) or [args()](Self::args) will be
44 /// substituted via `"$@"` if it's not already present in the command.
45 ///
46 /// If neither this method nor
47 /// [`command_may_be_shell_script()`](Self::command_may_be_shell_script()) is called,
48 /// commands are always executed verbatim and directly, without the use of a shell. (But
49 /// see [`command_may_be_shell_script()`](Self::command_may_be_shell_script()) on other
50 /// methods that call that method.)
51 ///
52 /// We also disallow manual argument splitting
53 /// (see [`command_may_be_shell_script_disallow_manual_argument_splitting`](Self::command_may_be_shell_script_disallow_manual_argument_splitting()))
54 /// to assure a shell is indeed used, no matter what.
55 pub fn with_shell(mut self) -> Self {
56 self.use_shell = true;
57 self.allow_manual_arg_splitting = false;
58 self
59 }
60
61 /// Quote the command if it is run in a shell, so its path is left intact.
62 ///
63 /// This is only meaningful if the command has been arranged to run in a shell, either
64 /// unconditionally with [`with_shell()`](Self::with_shell()), or conditionally with
65 /// [`command_may_be_shell_script()`](Self::command_may_be_shell_script()).
66 ///
67 /// Note that this should not be used if the command is a script - quoting is only the
68 /// right choice if it's known to be a program path.
69 ///
70 /// Note also that this does not affect arguments passed with [arg()](Self::arg) or
71 /// [args()](Self::args), which do not have to be quoted by the *caller* because they are
72 /// passed as `"$@"` positional parameters (`"$1"`, `"$2"`, and so on).
73 pub fn with_quoted_command(mut self) -> Self {
74 self.quote_command = true;
75 self
76 }
77
78 /// Set the name or path to the shell `program` to use if a shell is to be used, to avoid
79 /// using the default shell which is `sh`.
80 ///
81 /// Note that shells that are not Bourne-style cannot be expected to work correctly,
82 /// because POSIX shell syntax is assumed when searching for and conditionally adding
83 /// `"$@"` to receive arguments, where applicable (and in the behaviour of
84 /// [`with_quoted_command()`](Self::with_quoted_command()), if called).
85 pub fn with_shell_program(mut self, program: impl Into<OsString>) -> Self {
86 self.shell_program = Some(program.into());
87 self
88 }
89
90 /// Unconditionally turn off using the shell when spawning the command.
91 ///
92 /// Note that not using the shell is the default. So an effective use of this method
93 /// is some time after [`command_may_be_shell_script()`](Self::command_may_be_shell_script())
94 /// or [`with_shell()`](Self::with_shell()) was called.
95 pub fn without_shell(mut self) -> Self {
96 self.use_shell = false;
97 self
98 }
99
100 /// Set additional `ctx` to be used when spawning the process.
101 ///
102 /// Note that this is a must for most kind of commands that `git` usually spawns, as at
103 /// least they need to know the correct Git repository to function.
104 pub fn with_context(mut self, ctx: Context) -> Self {
105 self.context = Some(ctx);
106 self
107 }
108
109 /// Like [`command_may_be_shell_script()`](Self::command_may_be_shell_script()), but try to
110 /// split arguments by hand if this can be safely done without a shell.
111 ///
112 /// This is useful on platforms where spawning processes is slow, or where many processes
113 /// have to be spawned in a row which should be sped up. Manual argument splitting is
114 /// enabled by default on Windows only.
115 ///
116 /// Note that this does *not* check for the use of possible shell builtins. Commands may
117 /// fail or behave differently if they are available as shell builtins and no corresponding
118 /// external command exists, or the external command behaves differently.
119 /// Leading shell assignment words are applied to the environment when followed by a command.
120 pub fn command_may_be_shell_script_allow_manual_argument_splitting(mut self) -> Self {
121 self.allow_manual_arg_splitting = true;
122 self.command_may_be_shell_script()
123 }
124
125 /// Like [`command_may_be_shell_script()`](Self::command_may_be_shell_script()), but don't
126 /// allow to bypass the shell even if manual argument splitting can be performed safely.
127 pub fn command_may_be_shell_script_disallow_manual_argument_splitting(mut self) -> Self {
128 self.allow_manual_arg_splitting = false;
129 self.command_may_be_shell_script()
130 }
131
132 /// Configure the process to use `stdio` for _stdin_.
133 pub fn stdin(mut self, stdio: Stdio) -> Self {
134 self.stdin = stdio;
135 self
136 }
137 /// Configure the process to use `stdio` for _stdout_.
138 pub fn stdout(mut self, stdio: Stdio) -> Self {
139 self.stdout = stdio;
140 self
141 }
142 /// Configure the process to use `stdio` for _stderr_.
143 pub fn stderr(mut self, stdio: Stdio) -> Self {
144 self.stderr = stdio;
145 self
146 }
147
148 /// Add `arg` to the list of arguments to call the command with.
149 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
150 self.args.push(arg.into());
151 self
152 }
153
154 /// Add `args` to the list of arguments to call the command with.
155 pub fn args(mut self, args: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
156 self.args
157 .append(&mut args.into_iter().map(Into::into).collect::<Vec<_>>());
158 self
159 }
160
161 /// Add `key` with `value` to the environment of the spawned command.
162 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
163 self.env.push((key.into(), value.into()));
164 self
165 }
166}
167
168/// Finalization
169impl Prepare {
170 /// Spawn the command as configured.
171 pub fn spawn(self) -> std::io::Result<std::process::Child> {
172 let mut cmd = Command::from(self);
173 gix_trace::debug!(cmd = ?cmd);
174 cmd.spawn()
175 }
176}
177
178impl From<Prepare> for Command {
179 fn from(mut prep: Prepare) -> Command {
180 let mut inline_env = Vec::new();
181 let mut cmd = if prep.use_shell {
182 let split_args = prep
183 .allow_manual_arg_splitting
184 .then(|| {
185 let command = gix_path::os_str_into_bstr(&prep.command).ok()?;
186 if command.find_byteset(b"\\|&;<>()$`\n*?[#~%").is_none() {
187 crate::parse::command_line(command).ok()
188 } else {
189 None
190 }
191 })
192 .flatten();
193 match split_args {
194 Some(parsed) => {
195 let mut cmd = if cfg!(windows) {
196 windows_command(parsed.command, &prep.env, &parsed.env)
197 } else {
198 Command::new(parsed.command)
199 };
200 cmd.args(parsed.args);
201 inline_env = parsed.env;
202 cmd
203 }
204 None => {
205 let mut cmd = match prep.shell_program {
206 Some(shell) => Command::new(shell),
207 None => gix_path::env::shell_command(),
208 };
209 // Passed as `command_name` after `-c <script>`; the shell uses it
210 // as `$0`, which prefixes its own diagnostic messages. If the
211 // shell path has no extractable basename — reachable only via
212 // degenerate input like `""` or `/` — fall back to `_`, the
213 // conventional placeholder for an unused `$0`, rather than
214 // making a false claim about which shell is running.
215 let arg0 = std::path::Path::new(cmd.get_program())
216 .file_name()
217 .unwrap_or(std::ffi::OsStr::new("_"))
218 .to_os_string();
219 cmd.arg("-c");
220 if !prep.args.is_empty() {
221 if !gix_path::os_str_into_bstr(&prep.command).is_ok_and(|cmd| cmd.contains_str("$@")) {
222 if prep.quote_command {
223 if let Ok(command) = gix_path::os_str_into_bstr(&prep.command) {
224 prep.command = gix_path::from_bstring(gix_quote::single(command)).into();
225 }
226 }
227 prep.command.push(r#" "$@""#);
228 } else {
229 gix_trace::debug!(
230 r#"Will not add '"$@"' to '{:?}' as it seems to contain '$@' already"#,
231 prep.command
232 );
233 }
234 }
235 cmd.arg(prep.command);
236 cmd.arg(arg0);
237 cmd
238 }
239 }
240 } else if cfg!(windows) {
241 windows_command(prep.command, &prep.env, &[])
242 } else {
243 Command::new(prep.command)
244 };
245 // We never want to have terminals pop-up on Windows if this runs from a GUI application.
246 #[cfg(windows)]
247 {
248 use std::os::windows::process::CommandExt;
249 const CREATE_NO_WINDOW: u32 = 0x08000000;
250 cmd.creation_flags(CREATE_NO_WINDOW);
251 }
252 cmd.stdin(prep.stdin)
253 .stdout(prep.stdout)
254 .stderr(prep.stderr)
255 .envs(prep.env)
256 .args(prep.args);
257 if let Some(ctx) = prep.context {
258 if let Some(git_dir) = ctx.git_dir {
259 cmd.env("GIT_DIR", &git_dir);
260 }
261 if let Some(worktree_dir) = ctx.worktree_dir {
262 cmd.env("GIT_WORK_TREE", worktree_dir);
263 }
264 if let Some(value) = ctx.no_replace_objects {
265 cmd.env("GIT_NO_REPLACE_OBJECTS", usize::from(value).to_string());
266 }
267 if let Some(namespace) = ctx.ref_namespace {
268 cmd.env("GIT_NAMESPACE", gix_path::from_bstring(namespace));
269 }
270 if let Some(value) = ctx.literal_pathspecs {
271 cmd.env("GIT_LITERAL_PATHSPECS", usize::from(value).to_string());
272 }
273 if let Some(value) = ctx.glob_pathspecs {
274 cmd.env(
275 if value {
276 "GIT_GLOB_PATHSPECS"
277 } else {
278 "GIT_NOGLOB_PATHSPECS"
279 },
280 "1",
281 );
282 }
283 if let Some(value) = ctx.icase_pathspecs {
284 cmd.env("GIT_ICASE_PATHSPECS", usize::from(value).to_string());
285 }
286 if let Some(stderr) = ctx.stderr {
287 cmd.stderr(if stderr { Stdio::inherit() } else { Stdio::null() });
288 }
289 }
290 cmd.envs(inline_env);
291 cmd
292 }
293}
294
295/// Create a Windows command using Git-compatible `PATH` lookup and shebang dispatch.
296///
297/// The last `PATH` in `inline_env` overrides the last one in `env`, which overrides the inherited value (of this process).
298/// The selected `PATH` is searched in order. A resolved shebang script is launched through its interpreter, ignoring shebang
299/// arguments. If an explicit `PATH` does not resolve a bare command, the missing program remains anchored in its first entry
300/// so Rust's broader Windows lookup cannot find it elsewhere.
301fn windows_command(command: OsString, env: &[(OsString, OsString)], inline_env: &[(String, OsString)]) -> Command {
302 let explicit_joined_paths = inline_env
303 .iter()
304 .rev()
305 .find(|(name, _)| name.eq_ignore_ascii_case("PATH"))
306 .map(|(_, value)| value.as_os_str())
307 .or_else(|| {
308 env.iter()
309 .rev()
310 .find(|(name, _)| name.to_str().is_some_and(|name| name.eq_ignore_ascii_case("PATH")))
311 .map(|(_, value)| value.as_os_str())
312 });
313 let joined_paths = explicit_joined_paths
314 .map(Cow::Borrowed)
315 .or_else(|| std::env::var_os("PATH").map(Cow::Owned));
316 let looked_up = joined_paths
317 .as_deref()
318 .and_then(|joined_paths| win_path_lookup(command.as_ref(), joined_paths));
319 let program: Cow<'_, Path> = match (looked_up, explicit_joined_paths) {
320 // Use the manually resolved path.
321 (Some(program), _) => Cow::Owned(program),
322 // An explicit `PATH` miss must not fall back to `std::process::Command` broader Windows search.
323 (None, Some(explicit_joined_paths)) if is_bare_command(Path::new(&command)) => {
324 Cow::Owned(prevent_further_path_lookup(command.as_ref(), explicit_joined_paths))
325 }
326 // Preserve non-bare commands and let `std::process::Command` resolve bare commands without an explicit `PATH`.
327 (None, _) => Cow::Borrowed(command.as_ref()),
328 };
329 if let Some(shebang) = extract_interpreter(program.as_ref()) {
330 let mut cmd = Command::new(shebang.interpreter);
331 // Git for Windows ignores shebang arguments and passes only the script path.
332 cmd.arg(program.as_ref());
333 cmd
334 } else {
335 match program {
336 // Process lookup happens before the child's environment is installed, so an explicitly
337 // configured PATH must be handled here for ordinary executables as well.
338 Cow::Owned(program) if explicit_joined_paths.is_some() => Command::new(program),
339 _ => Command::new(command),
340 }
341 }
342}
343
344/// Represent the failed lookup of `command` in an explicitly assigned `PATH` without permitting another search.
345///
346/// `joined_paths` is the complete value of the explicit `PATH`, not one of its entries. The first non-empty entry is
347/// joined with `command`, producing a path that Rust's Windows resolver will not look up elsewhere. If there is no such
348/// entry, a trailing separator makes `command` invalid instead.
349fn prevent_further_path_lookup(command: &Path, joined_paths: &OsStr) -> PathBuf {
350 if let Some(mut root) = split_paths(joined_paths).next() {
351 root.push(command);
352 root
353 } else {
354 let mut missing = command.to_owned();
355 // A trailing separator makes the program invalid without allowing Rust to search ambient locations.
356 missing.push("");
357 missing
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
366 fn explicit_path_lookup_failure_stays_within_that_path() -> gix_testtools::Result {
367 let joined_paths = std::env::join_paths(["", "not/a/real/path", "also/not/real"])?;
368 let cmd = windows_command("missing.exe".into(), &[], &[("PATH".into(), joined_paths)]);
369 assert_eq!(
370 cmd.get_program(),
371 std::path::Path::new("not/a/real/path/missing.exe"),
372 "an inline PATH miss must not leave a bare program for Rust to resolve elsewhere"
373 );
374 Ok(())
375 }
376}