Skip to main content

worktree_io/opener/
shell.rs

1use anyhow::{bail, Context, Result};
2use std::process::{Command, Stdio};
3
4/// Return the `PATH` string augmented with common homebrew and system locations.
5///
6/// On Windows the system `PATH` is returned unchanged: homebrew paths do not
7/// apply and the separator is already `;`.
8#[must_use]
9pub fn augmented_path() -> String {
10    let current = std::env::var("PATH").unwrap_or_default();
11    #[cfg(windows)]
12    {
13        return current;
14    }
15    let extras = ["/usr/local/bin", "/opt/homebrew/bin", "/opt/homebrew/sbin"];
16    let mut parts: Vec<&str> = extras.to_vec();
17    for p in current.split(':').filter(|s| !s.is_empty()) {
18        if !parts.contains(&p) {
19            parts.push(p);
20        }
21    }
22    parts.join(":")
23}
24
25pub(super) fn run_shell_command(cmd: &str, background: bool) -> Result<()> {
26    let mut parts = shlex_split(cmd);
27    if parts.is_empty() {
28        bail!("Empty command");
29    }
30    let program = parts.remove(0);
31    let mut builder = Command::new(&program);
32    builder
33        .args(&parts)
34        .env("PATH", augmented_path())
35        .stdin(Stdio::null())
36        .stdout(Stdio::null())
37        .stderr(Stdio::null());
38    if background {
39        builder
40            .spawn()
41            .with_context(|| format!("Failed to spawn {program}"))?;
42    } else {
43        builder
44            .status()
45            .with_context(|| format!("Failed to run {program}"))?;
46    }
47    Ok(())
48}
49
50fn shlex_split(s: &str) -> Vec<String> {
51    let mut parts = Vec::new();
52    let mut current = String::new();
53    let mut in_quotes = false;
54    for c in s.chars() {
55        match c {
56            '"' => in_quotes = !in_quotes,
57            ' ' | '\t' if !in_quotes => {
58                if !current.is_empty() {
59                    parts.push(current.clone());
60                    current.clear();
61                }
62            }
63            _ => current.push(c),
64        }
65    }
66    if !current.is_empty() {
67        parts.push(current);
68    }
69    parts
70}
71
72#[cfg(test)]
73#[path = "shell_tests.rs"]
74mod tests;