worktree_io/opener/
shell.rs1use anyhow::{bail, Context, Result};
2use std::process::{Command, Stdio};
3
4#[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;