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