Skip to main content

worktree_io/opener/
shell.rs

1use anyhow::{bail, Context, Result};
2use std::process::{Command, Stdio};
3
4pub fn augmented_path() -> String {
5    let current = std::env::var("PATH").unwrap_or_default();
6    let extras = ["/usr/local/bin", "/opt/homebrew/bin", "/opt/homebrew/sbin"];
7    let mut parts: Vec<&str> = extras.to_vec();
8    for p in current.split(':').filter(|s| !s.is_empty()) {
9        if !parts.contains(&p) {
10            parts.push(p);
11        }
12    }
13    parts.join(":")
14}
15
16pub(super) fn run_shell_command(cmd: &str) -> Result<()> {
17    let mut parts = shlex_split(cmd);
18    if parts.is_empty() {
19        bail!("Empty command");
20    }
21    let program = parts.remove(0);
22    Command::new(&program)
23        .args(&parts)
24        .env("PATH", augmented_path())
25        .stdin(Stdio::null())
26        .stdout(Stdio::null())
27        .stderr(Stdio::null())
28        .spawn()
29        .with_context(|| format!("Failed to spawn {program}"))?;
30    Ok(())
31}
32
33fn shlex_split(s: &str) -> Vec<String> {
34    let mut parts = Vec::new();
35    let mut current = String::new();
36    let mut in_quotes = false;
37    for c in s.chars() {
38        match c {
39            '"' => in_quotes = !in_quotes,
40            ' ' | '\t' if !in_quotes => {
41                if !current.is_empty() {
42                    parts.push(current.clone());
43                    current.clear();
44                }
45            }
46            _ => current.push(c),
47        }
48    }
49    if !current.is_empty() {
50        parts.push(current);
51    }
52    parts
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    #[test]
59    fn test_shlex_simple() {
60        assert_eq!(
61            shlex_split("git commit -m init"),
62            vec!["git", "commit", "-m", "init"]
63        );
64    }
65    #[test]
66    fn test_shlex_quoted() {
67        assert_eq!(
68            shlex_split(r#"git commit -m "hello world""#),
69            vec!["git", "commit", "-m", "hello world"]
70        );
71    }
72    #[test]
73    fn test_shlex_tabs() {
74        assert_eq!(shlex_split("a\tb"), vec!["a", "b"]);
75    }
76    #[test]
77    fn test_shlex_empty() {
78        assert!(shlex_split("").is_empty());
79    }
80    #[test]
81    fn test_augmented_path_contains_homebrew() {
82        let p = augmented_path();
83        assert!(p.contains("/opt/homebrew/bin"));
84    }
85    #[test]
86    fn test_run_shell_command_empty() {
87        assert!(run_shell_command("").is_err());
88    }
89    #[test]
90    fn test_run_shell_command_success() {
91        run_shell_command("true").unwrap();
92    }
93    #[test]
94    fn test_run_shell_command_bad_program() {
95        assert!(run_shell_command("__nonexistent_xyz_wt__").is_err());
96    }
97}