Skip to main content

safe_chains/
compound.rs

1#[derive(Debug, Clone, PartialEq, Eq)]
2pub enum ShellUnit {
3    Simple(String),
4    For {
5        header: String,
6        body: Vec<ShellUnit>,
7    },
8    Loop {
9        kind: LoopKind,
10        condition: Vec<ShellUnit>,
11        body: Vec<ShellUnit>,
12    },
13    If {
14        branches: Vec<Branch>,
15        else_body: Vec<ShellUnit>,
16    },
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum LoopKind {
21    While,
22    Until,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Branch {
27    pub condition: Vec<ShellUnit>,
28    pub body: Vec<ShellUnit>,
29}
30
31pub fn parse<S: AsRef<str>>(segments: &[S]) -> Option<Vec<ShellUnit>> {
32    let strs: Vec<&str> = segments.iter().map(|s| s.as_ref()).collect();
33    parse_inner(&strs)
34}
35
36fn first_word(s: &str) -> &str {
37    s.split_whitespace().next().unwrap_or("")
38}
39
40fn rest_after_first_word(s: &str) -> &str {
41    let s = s.trim();
42    match s.find(char::is_whitespace) {
43        Some(pos) => s[pos..].trim_start(),
44        None => "",
45    }
46}
47
48fn opens_loop(s: &str) -> bool {
49    let fw = first_word(s);
50    matches!(fw, "for" | "while" | "until")
51        || (matches!(fw, "do" | "then" | "else" | "elif") && {
52            let rest = rest_after_first_word(s);
53            !rest.is_empty() && opens_loop(rest)
54        })
55}
56
57fn opens_if(s: &str) -> bool {
58    let fw = first_word(s);
59    fw == "if"
60        || (matches!(fw, "do" | "then" | "else" | "elif") && {
61            let rest = rest_after_first_word(s);
62            !rest.is_empty() && opens_if(rest)
63        })
64}
65
66fn find_do(segments: &[&str]) -> Option<usize> {
67    (1..segments.len()).find(|&i| first_word(segments[i]) == "do")
68}
69
70fn find_closing_done(segments: &[&str], do_pos: usize) -> Option<usize> {
71    let do_rest = rest_after_first_word(segments[do_pos]);
72    let mut depth: usize = 1;
73    if !do_rest.is_empty() && opens_loop(do_rest) {
74        depth += 1;
75    }
76    for (i, seg) in segments.iter().enumerate().skip(do_pos + 1) {
77        if opens_loop(seg) {
78            depth += 1;
79        } else if first_word(seg) == "done" {
80            depth -= 1;
81            if depth == 0 {
82                return Some(i);
83            }
84        }
85    }
86    None
87}
88
89fn find_closing_fi(segments: &[&str]) -> Option<usize> {
90    let mut depth: usize = 1;
91    for (i, seg) in segments.iter().enumerate().skip(1) {
92        if opens_if(seg) {
93            depth += 1;
94        } else if first_word(seg) == "fi" {
95            depth -= 1;
96            if depth == 0 {
97                return Some(i);
98            }
99        }
100    }
101    None
102}
103
104fn extract_body<'a>(segments: &[&'a str], do_pos: usize, close_pos: usize) -> Vec<&'a str> {
105    let mut body = Vec::new();
106    let do_rest = rest_after_first_word(segments[do_pos]);
107    if !do_rest.is_empty() {
108        body.push(do_rest);
109    }
110    body.extend_from_slice(&segments[(do_pos + 1)..close_pos]);
111    body
112}
113
114fn parse_for(segments: &[&str]) -> Option<(ShellUnit, usize)> {
115    let do_pos = find_do(segments)?;
116    let done_pos = find_closing_done(segments, do_pos)?;
117
118    let mut header_parts = Vec::new();
119    let first_rest = rest_after_first_word(segments[0]);
120    if !first_rest.is_empty() {
121        header_parts.push(first_rest);
122    }
123    for seg in &segments[1..do_pos] {
124        let trimmed = seg.trim();
125        if !trimmed.is_empty() {
126            header_parts.push(trimmed);
127        }
128    }
129    let header = header_parts.join(" ");
130
131    let body_segs = extract_body(segments, do_pos, done_pos);
132    let body = parse_inner(&body_segs)?;
133
134    Some((ShellUnit::For { header, body }, done_pos + 1))
135}
136
137fn parse_loop(segments: &[&str]) -> Option<(ShellUnit, usize)> {
138    let kind = match first_word(segments[0]) {
139        "while" => LoopKind::While,
140        "until" => LoopKind::Until,
141        _ => return None,
142    };
143
144    let do_pos = find_do(segments)?;
145    let done_pos = find_closing_done(segments, do_pos)?;
146
147    let mut cond_segs = Vec::new();
148    let first_rest = rest_after_first_word(segments[0]);
149    if !first_rest.is_empty() {
150        cond_segs.push(first_rest);
151    }
152    for seg in &segments[1..do_pos] {
153        let trimmed = seg.trim();
154        if !trimmed.is_empty() {
155            cond_segs.push(trimmed);
156        }
157    }
158    let condition = parse_inner(&cond_segs)?;
159
160    let body_segs = extract_body(segments, do_pos, done_pos);
161    let body = parse_inner(&body_segs)?;
162
163    Some((
164        ShellUnit::Loop {
165            kind,
166            condition,
167            body,
168        },
169        done_pos + 1,
170    ))
171}
172
173fn parse_if(segments: &[&str]) -> Option<(ShellUnit, usize)> {
174    let fi_pos = find_closing_fi(segments)?;
175
176    let mut depth = 0usize;
177    let mut markers: Vec<(usize, &str)> = Vec::new();
178
179    for (i, seg) in segments.iter().enumerate().take(fi_pos + 1) {
180        let fw = first_word(seg);
181
182        if depth == 0 && fw == "if" {
183            markers.push((i, "if"));
184        }
185        if depth == 1 {
186            match fw {
187                "then" | "elif" | "else" => markers.push((i, fw)),
188                _ => {}
189            }
190        }
191
192        if opens_if(seg) {
193            depth += 1;
194        }
195        if fw == "fi" {
196            depth = depth.checked_sub(1)?;
197        }
198    }
199
200    let mut branches = Vec::new();
201    let mut else_body = Vec::new();
202    let mut mi = 0;
203
204    while mi < markers.len() {
205        let (pos, kw) = markers[mi];
206        if !matches!(kw, "if" | "elif") {
207            return None;
208        }
209        mi += 1;
210
211        if mi >= markers.len() {
212            return None;
213        }
214        let (then_pos, then_kw) = markers[mi];
215        if then_kw != "then" {
216            return None;
217        }
218        mi += 1;
219
220        let mut cond_segs = Vec::new();
221        let rest = rest_after_first_word(segments[pos]);
222        if !rest.is_empty() {
223            cond_segs.push(rest);
224        }
225        for seg in &segments[(pos + 1)..then_pos] {
226            let trimmed = seg.trim();
227            if !trimmed.is_empty() {
228                cond_segs.push(trimmed);
229            }
230        }
231        let condition = parse_inner(&cond_segs)?;
232
233        let body_end = if mi < markers.len() {
234            markers[mi].0
235        } else {
236            fi_pos
237        };
238
239        let mut body_segs = Vec::new();
240        let then_rest = rest_after_first_word(segments[then_pos]);
241        if !then_rest.is_empty() {
242            body_segs.push(then_rest);
243        }
244        body_segs.extend_from_slice(&segments[(then_pos + 1)..body_end]);
245        let body = parse_inner(&body_segs)?;
246
247        branches.push(Branch { condition, body });
248
249        if mi < markers.len() && markers[mi].1 == "else" {
250            let else_pos = markers[mi].0;
251            let mut else_segs = Vec::new();
252            let else_rest = rest_after_first_word(segments[else_pos]);
253            if !else_rest.is_empty() {
254                else_segs.push(else_rest);
255            }
256            else_segs.extend_from_slice(&segments[(else_pos + 1)..fi_pos]);
257            else_body = parse_inner(&else_segs)?;
258            break;
259        }
260    }
261
262    if branches.is_empty() {
263        return None;
264    }
265
266    Some((ShellUnit::If { branches, else_body }, fi_pos + 1))
267}
268
269fn parse_inner(segments: &[&str]) -> Option<Vec<ShellUnit>> {
270    let mut result = Vec::new();
271    let mut i = 0;
272    while i < segments.len() {
273        let seg = segments[i].trim();
274        if seg.is_empty() {
275            i += 1;
276            continue;
277        }
278        match first_word(seg) {
279            "for" => {
280                let (unit, consumed) = parse_for(&segments[i..])?;
281                result.push(unit);
282                i += consumed;
283            }
284            "while" | "until" => {
285                let (unit, consumed) = parse_loop(&segments[i..])?;
286                result.push(unit);
287                i += consumed;
288            }
289            "if" => {
290                let (unit, consumed) = parse_if(&segments[i..])?;
291                result.push(unit);
292                i += consumed;
293            }
294            "do" | "done" | "then" | "elif" | "else" | "fi" => return None,
295            _ => {
296                result.push(ShellUnit::Simple(seg.to_string()));
297                i += 1;
298            }
299        }
300    }
301    Some(result)
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    fn segs(cmd: &str) -> Vec<String> {
309        crate::parse::CommandLine::new(cmd).segments().into_iter().map(|s| s.as_str().to_string()).collect()
310    }
311
312    #[test]
313    fn simple_commands() {
314        assert_eq!(
315            parse(&segs("echo hello; ls")),
316            Some(vec![
317                ShellUnit::Simple("echo hello".into()),
318                ShellUnit::Simple("ls".into()),
319            ])
320        );
321    }
322
323    #[test]
324    fn for_loop() {
325        assert_eq!(
326            parse(&segs("for x in 1 2 3; do echo $x; done")),
327            Some(vec![ShellUnit::For {
328                header: "x in 1 2 3".into(),
329                body: vec![ShellUnit::Simple("echo $x".into())],
330            }])
331        );
332    }
333
334    #[test]
335    fn for_empty_body() {
336        assert_eq!(
337            parse(&segs("for x in 1 2 3; do; done")),
338            Some(vec![ShellUnit::For {
339                header: "x in 1 2 3".into(),
340                body: vec![],
341            }])
342        );
343    }
344
345    #[test]
346    fn for_multi_body() {
347        assert_eq!(
348            parse(&segs("for f in *.txt; do cat $f | grep pattern; done")),
349            Some(vec![ShellUnit::For {
350                header: "f in *.txt".into(),
351                body: vec![
352                    ShellUnit::Simple("cat $f".into()),
353                    ShellUnit::Simple("grep pattern".into()),
354                ],
355            }])
356        );
357    }
358
359    #[test]
360    fn sequential_for_loops() {
361        let result = parse(&segs("for x in 1 2; do echo $x; done; for y in a b; do echo $y; done"));
362        assert!(result.is_some());
363        let units = result.unwrap();
364        assert_eq!(units.len(), 2);
365        assert!(matches!(&units[0], ShellUnit::For { header, .. } if header == "x in 1 2"));
366        assert!(matches!(&units[1], ShellUnit::For { header, .. } if header == "y in a b"));
367    }
368
369    #[test]
370    fn nested_for_loops() {
371        let result = parse(&segs("for x in 1 2; do for y in a b; do echo $x $y; done; done"));
372        assert_eq!(
373            result,
374            Some(vec![ShellUnit::For {
375                header: "x in 1 2".into(),
376                body: vec![ShellUnit::For {
377                    header: "y in a b".into(),
378                    body: vec![ShellUnit::Simple("echo $x $y".into())],
379                }],
380            }])
381        );
382    }
383
384    #[test]
385    fn for_then_command() {
386        let result = parse(&segs("for x in 1 2; do echo $x; done && echo finished"));
387        assert!(result.is_some());
388        let units = result.unwrap();
389        assert_eq!(units.len(), 2);
390        assert!(matches!(&units[0], ShellUnit::For { .. }));
391        assert_eq!(units[1], ShellUnit::Simple("echo finished".into()));
392    }
393
394    #[test]
395    fn while_loop() {
396        assert_eq!(
397            parse(&segs("while test -f /tmp/foo; do sleep 1; done")),
398            Some(vec![ShellUnit::Loop {
399                kind: LoopKind::While,
400                condition: vec![ShellUnit::Simple("test -f /tmp/foo".into())],
401                body: vec![ShellUnit::Simple("sleep 1".into())],
402            }])
403        );
404    }
405
406    #[test]
407    fn until_loop() {
408        assert_eq!(
409            parse(&segs("until test -f /tmp/ready; do sleep 1; done")),
410            Some(vec![ShellUnit::Loop {
411                kind: LoopKind::Until,
412                condition: vec![ShellUnit::Simple("test -f /tmp/ready".into())],
413                body: vec![ShellUnit::Simple("sleep 1".into())],
414            }])
415        );
416    }
417
418    #[test]
419    fn if_then_fi() {
420        assert_eq!(
421            parse(&segs("if test -f foo; then echo exists; fi")),
422            Some(vec![ShellUnit::If {
423                branches: vec![Branch {
424                    condition: vec![ShellUnit::Simple("test -f foo".into())],
425                    body: vec![ShellUnit::Simple("echo exists".into())],
426                }],
427                else_body: vec![],
428            }])
429        );
430    }
431
432    #[test]
433    fn if_then_else_fi() {
434        assert_eq!(
435            parse(&segs("if test -f foo; then echo yes; else echo no; fi")),
436            Some(vec![ShellUnit::If {
437                branches: vec![Branch {
438                    condition: vec![ShellUnit::Simple("test -f foo".into())],
439                    body: vec![ShellUnit::Simple("echo yes".into())],
440                }],
441                else_body: vec![ShellUnit::Simple("echo no".into())],
442            }])
443        );
444    }
445
446    #[test]
447    fn if_elif_else() {
448        let result = parse(&segs("if test -f a; then echo a; elif test -f b; then echo b; else echo c; fi"));
449        assert!(result.is_some());
450        let units = result.unwrap();
451        assert_eq!(units.len(), 1);
452        if let ShellUnit::If { branches, else_body } = &units[0] {
453            assert_eq!(branches.len(), 2);
454            assert_eq!(else_body, &[ShellUnit::Simple("echo c".into())]);
455        } else {
456            panic!("expected If");
457        }
458    }
459
460    #[test]
461    fn nested_if_in_for() {
462        let result = parse(&segs("for x in 1 2; do if test $x = 1; then echo one; fi; done"));
463        assert!(result.is_some());
464        let units = result.unwrap();
465        assert_eq!(units.len(), 1);
466        if let ShellUnit::For { body, .. } = &units[0] {
467            assert_eq!(body.len(), 1);
468            assert!(matches!(&body[0], ShellUnit::If { .. }));
469        } else {
470            panic!("expected For");
471        }
472    }
473
474    #[test]
475    fn nested_for_in_if() {
476        let result = parse(&segs("if true; then for x in 1 2; do echo $x; done; fi"));
477        assert!(result.is_some());
478        let units = result.unwrap();
479        assert_eq!(units.len(), 1);
480        if let ShellUnit::If { branches, .. } = &units[0] {
481            assert_eq!(branches.len(), 1);
482            assert_eq!(branches[0].body.len(), 1);
483            assert!(matches!(&branches[0].body[0], ShellUnit::For { .. }));
484        } else {
485            panic!("expected If");
486        }
487    }
488
489    #[test]
490    fn keyword_as_data() {
491        assert_eq!(
492            parse(&segs("echo for; echo done; echo if; echo fi")),
493            Some(vec![
494                ShellUnit::Simple("echo for".into()),
495                ShellUnit::Simple("echo done".into()),
496                ShellUnit::Simple("echo if".into()),
497                ShellUnit::Simple("echo fi".into()),
498            ])
499        );
500    }
501
502    #[test]
503    fn stray_done() {
504        assert_eq!(parse(&segs("echo hello; done")), None);
505    }
506
507    #[test]
508    fn stray_fi() {
509        assert_eq!(parse(&segs("fi")), None);
510    }
511
512    #[test]
513    fn unclosed_for() {
514        assert_eq!(parse(&segs("for x in 1 2 3; do echo $x")), None);
515    }
516
517    #[test]
518    fn unclosed_if() {
519        assert_eq!(parse(&segs("if true; then echo hello")), None);
520    }
521
522    #[test]
523    fn for_missing_do() {
524        assert_eq!(parse(&segs("for x in 1 2 3; echo $x; done")), None);
525    }
526
527    #[test]
528    fn triple_nested_for() {
529        let result = parse(&segs(
530            "for x in 1; do for y in 2; do for z in 3; do echo $x $y $z; done; done; done"
531        ));
532        assert!(result.is_some());
533        let units = result.unwrap();
534        if let ShellUnit::For { body, .. } = &units[0] {
535            if let ShellUnit::For { body, .. } = &body[0] {
536                if let ShellUnit::For { body, .. } = &body[0] {
537                    assert_eq!(body, &[ShellUnit::Simple("echo $x $y $z".into())]);
538                } else {
539                    panic!("expected innermost For");
540                }
541            } else {
542                panic!("expected middle For");
543            }
544        } else {
545            panic!("expected outer For");
546        }
547    }
548
549    #[test]
550    fn while_negation() {
551        let result = parse(&segs("while ! test -f /tmp/done; do sleep 1; done"));
552        assert!(result.is_some());
553        if let ShellUnit::Loop { condition, .. } = &result.unwrap()[0] {
554            assert_eq!(condition, &[ShellUnit::Simple("! test -f /tmp/done".into())]);
555        } else {
556            panic!("expected Loop");
557        }
558    }
559}