Skip to main content

gix_command/
lib.rs

1//! Launch commands very similarly to `Command`, but with `git` specific capabilities and adjustments.
2//!
3//! ## Examples
4//!
5//! ```
6//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
7//! let output = gix_command::prepare("git")
8//!     .arg("--version")
9//!     .spawn()?
10//!     .wait_with_output()?;
11//!
12//! assert!(output.status.success());
13//! assert!(String::from_utf8(output.stdout)?.starts_with("git version "));
14//! # Ok(()) }
15//! ```
16#![deny(missing_docs)]
17#![forbid(unsafe_code)]
18
19use std::{
20    ffi::{OsStr, OsString},
21    io::Read,
22    path::{Path, PathBuf},
23};
24
25use bstr::{BString, ByteSlice};
26
27///
28pub mod parse;
29
30mod prepare;
31
32///
33pub mod shebang {
34    use std::{ffi::OsString, path::PathBuf};
35
36    use bstr::{BStr, ByteSlice};
37
38    /// Parse `buf` to extract all shebang information.
39    pub fn parse(buf: &BStr) -> Option<Data> {
40        let mut line = buf.lines().next()?;
41        line = line.strip_prefix(b"#!")?;
42
43        let slash_idx = line.rfind_byteset(br"/\")?;
44        let space_idx = line[slash_idx..]
45            .find_byte(b' ')
46            .map_or(line.len(), |space_idx| slash_idx + space_idx);
47        let (interpreter, args) = line.split_at(space_idx);
48        Some(Data {
49            interpreter: gix_path::from_byte_slice(interpreter.trim()).ok()?.to_owned(),
50            args: crate::parse::arguments(args.trim().as_bstr()).unwrap_or_default(),
51        })
52    }
53
54    /// Shebang information as [parsed](parse()) from a buffer that should contain at least one line.
55    #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
56    pub struct Data {
57        /// The interpreter to run.
58        pub interpreter: PathBuf,
59        /// Arguments following the interpreter, split like [`crate::parse::command_line()`], or empty if malformed.
60        pub args: Vec<OsString>,
61    }
62}
63
64/// A structure to keep settings to use when invoking a command via [`spawn()`][Prepare::spawn()],
65/// after creating it with [`prepare()`].
66///
67/// Convert it to [`std::process::Command`] with [`TryFrom::try_from()`] to finish preparation
68/// without spawning. Preparation reports an error when shell quoting or namespace conversion
69/// requires an encoding the platform cannot represent.
70pub struct Prepare {
71    /// The command to invoke, either directly or with a shell depending on `use_shell`.
72    pub command: OsString,
73    /// Additional information to be passed to the spawned command.
74    pub context: Option<Context>,
75    /// The way standard input is configured.
76    pub stdin: std::process::Stdio,
77    /// The way standard output is configured.
78    pub stdout: std::process::Stdio,
79    /// The way standard error is configured.
80    pub stderr: std::process::Stdio,
81    /// The arguments to pass to the process being spawned.
82    pub args: Vec<OsString>,
83    /// Environment variables to set for the spawned process.
84    pub env: Vec<(OsString, OsString)>,
85    /// If `true`, we will use `shell_program` or `sh` to execute the `command`.
86    pub use_shell: bool,
87    /// If `true`, `command` is assumed to be a command or path to the program to execute, and it
88    /// will be shell-quoted to assure it will be executed as is and without splitting across
89    /// whitespace.
90    pub quote_command: bool,
91    /// The name or path to the shell program to use instead of `sh`.
92    pub shell_program: Option<OsString>,
93    /// If `true` (default `true` on Windows and `false` everywhere else) we will see if it's safe
94    /// to manually invoke `command` after splitting its arguments as a shell would do.
95    ///
96    /// Note that outside of Windows, it's generally not advisable as this removes support for
97    /// literal shell scripts with shell-builtins.
98    ///
99    /// This mimics the behaviour we see with `git` on Windows, which also won't invoke the shell
100    /// there at all.
101    ///
102    /// Only effective if `use_shell` is `true` as well, as the shell will be used as a fallback if
103    /// it's not possible to split arguments as the command-line contains 'scripting'.
104    pub allow_manual_arg_splitting: bool,
105}
106
107/// Additional information that is relevant to spawned processes, which typically receive
108/// a wealth of contextual information when spawned from `git`.
109///
110/// Repository paths must be absolute, resolved against the caller's captured working directory.
111/// Consumers may change the child process's working directory, so relative paths are not reliable.
112///
113/// See [the git source code](https://github.com/git/git/blob/cfb8a6e9a93adbe81efca66e6110c9b4d2e57169/git.c#L191)
114/// for details.
115#[derive(Debug, Default, Clone)]
116pub struct Context {
117    /// The absolute path to the `.git` directory that contains the repository.
118    ///
119    /// If set, it will be used to set the `GIT_DIR` environment variable.
120    pub git_dir: Option<PathBuf>,
121    /// Set the `GIT_WORK_TREE` environment variable with the given absolute path.
122    pub worktree_dir: Option<PathBuf>,
123    /// If `true`, set `GIT_NO_REPLACE_OBJECTS` to `1`, which turns off object replacements, or `0` otherwise.
124    /// If `None`, the variable won't be set.
125    pub no_replace_objects: Option<bool>,
126    /// Set the `GIT_NAMESPACE` variable with the given value, effectively namespacing all
127    /// operations on references.
128    pub ref_namespace: Option<BString>,
129    /// If `true`, set `GIT_LITERAL_PATHSPECS` to `1`, which makes globs literal and prefixes as well, or `0` otherwise.
130    /// If `None`, the variable won't be set.
131    pub literal_pathspecs: Option<bool>,
132    /// If `true`, set `GIT_GLOB_PATHSPECS` to `1`, which lets wildcards not match the `/` character, and equals the `:(glob)` prefix.
133    /// If `false`, set `GIT_NOGLOB_PATHSPECS` to `1` which lets globs match only themselves.
134    /// If `None`, the variable won't be set.
135    pub glob_pathspecs: Option<bool>,
136    /// If `true`, set `GIT_ICASE_PATHSPECS` to `1`, to let patterns match case-insensitively, or `0` otherwise.
137    /// If `None`, the variable won't be set.
138    pub icase_pathspecs: Option<bool>,
139    /// If `true`, inherit `stderr` just like it's the default when spawning processes.
140    /// If `false`, suppress all stderr output.
141    /// If not `None`, this will override any value set with [`Prepare::stderr()`].
142    pub stderr: Option<bool>,
143}
144
145#[cfg(windows)]
146fn is_exe(executable: &Path) -> bool {
147    executable.extension() == Some(std::ffi::OsStr::new("exe"))
148}
149
150/// Split a joined `PATH` value according to platform conventions, omitting empty entries.
151///
152/// Git's Windows lookup skips empty entries instead of treating them as the current directory. This also prevents an
153/// explicitly empty `PATH` from finding a command there.
154fn split_paths(joined_paths: &OsStr) -> impl Iterator<Item = PathBuf> + '_ {
155    std::env::split_paths(joined_paths).filter(|path| !path.as_os_str().is_empty())
156}
157
158/// Return whether `command` is a single path component eligible for `PATH` lookup.
159fn is_bare_command(command: &Path) -> bool {
160    command.components().take(2).count() == 1
161}
162
163/// Try to find `command` in `joined_paths` using [`split_paths()`].
164/// Commands with an explicit extension are matched verbatim. Otherwise, `.exe` is preferred over an extensionless file.
165/// Note that just like Git, no lookup is performed if a slash or backslash is in `command`.
166fn win_path_lookup(command: &Path, joined_paths: &std::ffi::OsStr) -> Option<PathBuf> {
167    fn lookup(root: &Path, command: &Path, has_extension: bool) -> Option<PathBuf> {
168        let mut path = root.join(command);
169        if has_extension {
170            return path.is_file().then_some(path);
171        }
172
173        path.set_extension("exe");
174        if path.is_file() {
175            return Some(path);
176        }
177        path.set_extension("");
178        path.is_file().then_some(path)
179    }
180    if !is_bare_command(command) {
181        return None;
182    }
183    let has_extension = command.extension().is_some();
184
185    for root in split_paths(joined_paths) {
186        if let Some(executable) = lookup(&root, command, has_extension) {
187            return Some(executable);
188        }
189    }
190    None
191}
192
193/// Parse the shebang (`#!<path>`) from the first line of `executable`, and return the shebang
194/// data when available.
195pub fn extract_interpreter(executable: &Path) -> Option<shebang::Data> {
196    #[cfg(windows)]
197    if is_exe(executable) {
198        return None;
199    }
200    let mut buf = [0; 100]; // Note: just like Git
201    let mut file = std::fs::File::open(executable).ok()?;
202    let n = file.read(&mut buf).ok()?;
203    shebang::parse(buf[..n].as_bstr())
204}
205
206/// Prepare `cmd` for [spawning][std::process::Command::spawn()] by configuring it with various builder methods.
207///
208/// Note that the default IO is configured for typical API usage, that is
209///
210/// - `stdin` is null to prevent blocking unexpectedly on consumption of stdin
211/// - `stdout` is captured for consumption by the caller
212/// - `stderr` is inherited to allow the command to provide context to the user
213///
214/// On Windows, terminal Windows will be suppressed automatically.
215///
216/// ### Warning
217///
218/// When using this method, be sure that the invoked program doesn't rely on the current working dir and/or
219/// environment variables to know its context. If so, call instead [`Prepare::with_context()`] to provide
220/// additional information.
221pub fn prepare(cmd: impl Into<OsString>) -> Prepare {
222    Prepare {
223        command: cmd.into(),
224        shell_program: None,
225        context: None,
226        stdin: std::process::Stdio::null(),
227        stdout: std::process::Stdio::piped(),
228        stderr: std::process::Stdio::inherit(),
229        args: Vec::new(),
230        env: Vec::new(),
231        use_shell: false,
232        quote_command: false,
233        allow_manual_arg_splitting: cfg!(windows),
234    }
235}
236
237#[cfg(test)]
238mod tests;