worktree_io/opener/
shell.rs1use 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}