Skip to main content

bctx_forge/intercept/
chain.rs

1/// Split a shell command string on `&&`, `||`, `;` and `|` boundaries.
2pub fn split_pipeline(cmd: &str) -> Vec<String> {
3    let mut parts = Vec::new();
4    let mut current = String::new();
5    let chars: Vec<char> = cmd.chars().collect();
6    let mut i = 0;
7
8    while i < chars.len() {
9        match chars[i] {
10            '&' if i + 1 < chars.len() && chars[i + 1] == '&' => {
11                let trimmed = current.trim().to_string();
12                if !trimmed.is_empty() {
13                    parts.push(trimmed);
14                }
15                current.clear();
16                i += 2;
17            }
18            '|' if i + 1 < chars.len() && chars[i + 1] == '|' => {
19                let trimmed = current.trim().to_string();
20                if !trimmed.is_empty() {
21                    parts.push(trimmed);
22                }
23                current.clear();
24                i += 2;
25            }
26            '|' | ';' => {
27                let trimmed = current.trim().to_string();
28                if !trimmed.is_empty() {
29                    parts.push(trimmed);
30                }
31                current.clear();
32                i += 1;
33            }
34            c => {
35                current.push(c);
36                i += 1;
37            }
38        }
39    }
40
41    let trimmed = current.trim().to_string();
42    if !trimmed.is_empty() {
43        parts.push(trimmed);
44    }
45    parts
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn splits_and_chain() {
54        let parts = split_pipeline("git status && cargo test");
55        assert_eq!(parts, vec!["git status", "cargo test"]);
56    }
57
58    #[test]
59    fn splits_pipe() {
60        let parts = split_pipeline("cargo build | grep error");
61        assert_eq!(parts, vec!["cargo build", "grep error"]);
62    }
63}