Skip to main content

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}