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}