1use std::collections::HashSet;
13
14use super::lexer::{tokenize, Token, TokenType};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Node {
19 pub term: String,
21 pub text: Option<String>,
23 pub children: Vec<Node>,
24}
25
26impl Node {
27 pub fn new(term: &str, children: Vec<Node>) -> Self {
28 Node {
29 term: term.to_string(),
30 text: None,
31 children,
32 }
33 }
34
35 pub fn with_text(term: &str, children: Vec<Node>, text: impl Into<String>) -> Self {
36 Node {
37 term: term.to_string(),
38 text: Some(text.into()),
39 children,
40 }
41 }
42
43 pub fn leaf(term: &str, text: impl Into<String>) -> Self {
44 Node::with_text(term, Vec::new(), text)
45 }
46}
47
48const BLOCK_TERMINATORS: [&str; 8] = ["then", "elif", "else", "fi", "do", "done", "esac", "}"];
50
51fn match_assignment(text: &str) -> Option<(String, String)> {
53 let equals = text.find('=')?;
54 let (name, value) = text.split_at(equals);
55 if !is_identifier(name) {
56 return None;
57 }
58 Some((name.to_string(), value[1..].to_string()))
59}
60
61fn is_identifier(text: &str) -> bool {
62 let mut characters = text.chars();
63 matches!(characters.next(), Some(first) if first.is_ascii_alphabetic() || first == '_')
64 && characters.all(|character| character.is_ascii_alphanumeric() || character == '_')
65}
66
67struct ShellParser {
68 tokens: Vec<Token>,
69 position: usize,
70}
71
72struct CommandParts {
74 assignments: Vec<Node>,
75 words: Vec<Node>,
76 redirects: Vec<Node>,
77}
78
79impl ShellParser {
80 fn new(source: &str) -> Self {
81 ShellParser {
82 tokens: tokenize(source),
83 position: 0,
84 }
85 }
86
87 fn current(&self) -> &Token {
88 &self.tokens[self.position]
89 }
90
91 fn at(&self, token_type: TokenType) -> bool {
92 self.current().token_type == token_type
93 }
94
95 fn at_text(&self, token_type: TokenType, text: &str) -> bool {
96 self.at(token_type) && self.current().text == text
97 }
98
99 fn at_word(&self, text: &str) -> bool {
100 self.at_text(TokenType::Word, text)
101 }
102
103 fn consume(&mut self) -> Token {
104 let token = self.tokens[self.position].clone();
105 self.position += 1;
106 token
107 }
108
109 fn expect_word(&mut self, text: &str) -> bool {
110 if self.at_word(text) {
111 self.consume();
112 true
113 } else {
114 false
115 }
116 }
117
118 fn skip_separators(&mut self, collector: Option<&mut Vec<Node>>) {
120 let mut newlines = 0usize;
121 let mut collector = collector;
122 loop {
123 if self.at(TokenType::Newline) {
124 self.consume();
125 newlines += 1;
126 if newlines > 1 {
129 if let Some(statements) = collector.as_deref_mut() {
130 statements.push(Node::new("blank", Vec::new()));
131 }
132 }
133 continue;
134 }
135 if self.at_text(TokenType::Operator, ";") {
136 self.consume();
137 continue;
138 }
139 return;
140 }
141 }
142
143 fn at_block_end(&self, extra_terminators: &HashSet<String>) -> bool {
145 let token = self.current();
146 if token.token_type == TokenType::Eof {
147 return true;
148 }
149 if token.token_type == TokenType::Operator && (token.text == ")" || token.text == ";;") {
150 return true;
151 }
152 if token.token_type != TokenType::Word {
153 return false;
154 }
155 BLOCK_TERMINATORS.contains(&token.text.as_str())
156 || extra_terminators.contains(token.text.as_str())
157 }
158
159 fn parse_script(&mut self) -> Node {
160 let statements = self.parse_statements(&HashSet::new());
161 Node::new("script", statements)
162 }
163
164 fn parse_statements(&mut self, extra_terminators: &HashSet<String>) -> Vec<Node> {
165 let mut statements = Vec::new();
166 self.skip_separators(None);
168
169 while !self.at_block_end(extra_terminators) {
170 if self.at(TokenType::Comment) {
171 let text = self.consume().text;
172 let body = text.trim_start_matches('#');
173 let body = body.strip_prefix(' ').unwrap_or(body);
174 statements.push(Node::leaf("comment", body));
175 } else {
176 match self.parse_list() {
177 Some(statement) => statements.push(statement),
178 None => break,
179 }
180 }
181 self.skip_separators(Some(&mut statements));
182 }
183
184 while statements.last().is_some_and(|last| last.term == "blank") {
186 statements.pop();
187 }
188 statements
189 }
190
191 fn parse_list(&mut self) -> Option<Node> {
193 let mut left = self.parse_pipeline()?;
194 while self.at_text(TokenType::Operator, "&&") || self.at_text(TokenType::Operator, "||") {
195 let operator = if self.consume().text == "&&" {
196 "and"
197 } else {
198 "or"
199 };
200 while self.at(TokenType::Newline) {
202 self.consume();
203 }
204 match self.parse_pipeline() {
205 Some(right) => left = Node::new(operator, vec![left, right]),
206 None => break,
207 }
208 }
209 Some(left)
210 }
211
212 fn parse_pipeline(&mut self) -> Option<Node> {
213 let mut stages = vec![self.parse_command()?];
214 while self.at_text(TokenType::Operator, "|") {
215 self.consume();
216 while self.at(TokenType::Newline) {
217 self.consume();
218 }
219 match self.parse_command() {
220 Some(stage) => stages.push(stage),
221 None => break,
222 }
223 }
224 if stages.len() == 1 {
225 stages.pop()
226 } else {
227 Some(Node::new("pipeline", stages))
228 }
229 }
230
231 fn parse_command(&mut self) -> Option<Node> {
232 if self.at_text(TokenType::Operator, "(") {
233 self.consume();
234 let body = self.parse_statements(&HashSet::new());
235 if self.at_text(TokenType::Operator, ")") {
236 self.consume();
237 }
238 return Some(Node::new("subshell", vec![Node::new("block", body)]));
239 }
240 if self.at_word("{") {
241 self.consume();
242 let body = self.parse_statements(&HashSet::new());
243 self.expect_word("}");
244 return Some(Node::new("block", body));
245 }
246 if self.at_word("if") {
247 return Some(self.parse_if());
248 }
249 if self.at_word("while") || self.at_word("until") {
250 return Some(self.parse_loop());
251 }
252 if self.at_word("for") {
253 return Some(self.parse_for());
254 }
255 if self.at_word("case") {
256 return Some(self.parse_case());
257 }
258 if let Some(function) = self.try_parse_function() {
259 return Some(function);
260 }
261 self.parse_simple_command()
262 }
263
264 fn parse_if(&mut self) -> Node {
266 self.consume(); let condition = self.parse_condition("then");
268 let consequent = Node::new("block", self.parse_statements(&HashSet::new()));
269
270 if self.at_word("elif") {
271 let alternative = self.parse_if();
272 return Node::new("if", vec![condition, consequent, alternative]);
273 }
274 let alternative = if self.expect_word("else") {
275 Some(Node::new("block", self.parse_statements(&HashSet::new())))
276 } else {
277 None
278 };
279 self.expect_word("fi");
280 match alternative {
281 Some(alternative) => Node::new("if", vec![condition, consequent, alternative]),
282 None => Node::new("if", vec![condition, consequent]),
283 }
284 }
285
286 fn parse_loop(&mut self) -> Node {
287 let term = if self.consume().text == "while" {
288 "while"
289 } else {
290 "until"
291 };
292 let condition = self.parse_condition("do");
293 let body = Node::new("block", self.parse_statements(&HashSet::new()));
294 self.expect_word("done");
295 Node::new(term, vec![condition, body])
296 }
297
298 fn parse_for(&mut self) -> Node {
300 self.consume(); let variable = if self.at(TokenType::Word) {
302 self.consume().text
303 } else {
304 "_".to_string()
305 };
306 let mut items = Vec::new();
307 if self.expect_word("in") {
308 while self.at(TokenType::Word)
309 && !BLOCK_TERMINATORS.contains(&self.current().text.as_str())
310 {
311 let text = self.consume().text;
312 items.push(Node::leaf("word", text));
313 }
314 }
315 self.skip_separators(None);
316 self.expect_word("do");
317 let body = Node::new("block", self.parse_statements(&HashSet::new()));
318 self.expect_word("done");
319 Node::new(
320 "for",
321 vec![
322 Node::leaf("name", variable),
323 Node::new("word-list", items),
324 body,
325 ],
326 )
327 }
328
329 fn parse_case(&mut self) -> Node {
331 self.consume(); let subject = if self.at(TokenType::Word) {
333 let text = self.consume().text;
334 Node::leaf("word", text)
335 } else {
336 Node::leaf("word", "")
337 };
338 self.expect_word("in");
339 self.skip_separators(None);
340
341 let mut branches = Vec::new();
342 while !self.at_word("esac") && !self.at(TokenType::Eof) {
343 branches.push(self.parse_case_branch());
344 }
345 self.expect_word("esac");
346 Node::new(
347 "case",
348 vec![subject, Node::new("case-branch-list", branches)],
349 )
350 }
351
352 fn parse_case_branch(&mut self) -> Node {
353 let mut patterns = Vec::new();
355 while self.at(TokenType::Word) {
356 let pattern = self.consume().text;
358 let term = if pattern == "*" {
359 "pattern-default"
360 } else {
361 "pattern"
362 };
363 patterns.push(Node::leaf(term, pattern));
364 if self.at_text(TokenType::Operator, "|") {
365 self.consume();
366 continue;
367 }
368 break;
369 }
370 if self.at_text(TokenType::Operator, ")") {
371 self.consume();
372 }
373 let body = Node::new("block", self.parse_statements(&HashSet::new()));
374 if self.at_text(TokenType::Operator, ";;") {
375 self.consume();
376 }
377 self.skip_separators(None);
378 Node::new(
379 "case-branch",
380 vec![Node::new("pattern-list", patterns), body],
381 )
382 }
383
384 fn parse_keyword_function_name(&mut self) -> Option<String> {
386 self.consume();
387 if !self.at(TokenType::Word) {
388 return None;
389 }
390 let name = self.consume().text;
391 if self.at_text(TokenType::Operator, "(") {
392 self.consume();
393 if self.at_text(TokenType::Operator, ")") {
394 self.consume();
395 }
396 }
397 Some(name)
398 }
399
400 fn parse_bare_function_name(&mut self) -> Option<String> {
402 let is_operator = |token: Option<&Token>, text: &str| {
403 token.is_some_and(|token| token.token_type == TokenType::Operator && token.text == text)
404 };
405
406 if !self.at(TokenType::Word)
407 || !is_identifier(&self.current().text)
408 || !is_operator(self.tokens.get(self.position + 1), "(")
409 || !is_operator(self.tokens.get(self.position + 2), ")")
410 {
411 return None;
412 }
413 let name = self.consume().text;
414 self.consume();
415 self.consume();
416 Some(name)
417 }
418
419 fn try_parse_function(&mut self) -> Option<Node> {
421 let start = self.position;
422 let name = if self.at_word("function") {
423 self.parse_keyword_function_name()
424 } else {
425 self.parse_bare_function_name()
426 };
427
428 let Some(name) = name else {
429 self.position = start;
430 return None;
431 };
432
433 self.skip_separators(None);
434 if !self.expect_word("{") {
435 self.position = start;
436 return None;
437 }
438 let body = Node::new("block", self.parse_statements(&HashSet::new()));
439 self.expect_word("}");
440 Some(Node::new("function", vec![Node::leaf("name", name), body]))
441 }
442
443 fn parse_condition(&mut self, introducer: &str) -> Node {
446 let terminators: HashSet<String> = HashSet::from([introducer.to_string()]);
447 let mut condition = self.parse_statements(&terminators);
448 self.skip_separators(None);
449 self.expect_word(introducer);
450 if condition.len() == 1 {
451 return condition.remove(0);
452 }
453 Node::new("block", condition)
454 }
455
456 fn at_command_end(&self, words: &[Node]) -> bool {
461 if words.is_empty() && self.at_block_end(&HashSet::new()) {
466 return true;
467 }
468 self.at(TokenType::Eof)
469 || self.at(TokenType::Newline)
470 || self.at(TokenType::Comment)
471 || self.at(TokenType::Operator)
472 }
473
474 fn parse_command_parts(&mut self) -> CommandParts {
476 let mut parts = CommandParts {
477 assignments: Vec::new(),
478 words: Vec::new(),
479 redirects: Vec::new(),
480 };
481
482 while !self.at_command_end(&parts.words) {
483 if self.at(TokenType::Redirect) {
484 let operator = self.consume().text;
485 let target = if self.at(TokenType::Word) {
486 self.consume().text
487 } else {
488 String::new()
489 };
490 parts.redirects.push(Node::with_text(
491 "redirect",
492 vec![Node::leaf("word", target)],
493 operator,
494 ));
495 continue;
496 }
497
498 let token = self.consume();
499 let assignment = if parts.words.is_empty() {
500 match_assignment(&token.text)
501 } else {
502 None
503 };
504 if let Some((name, value)) = assignment {
505 parts.assignments.push(Node::with_text(
506 "assignment",
507 vec![Node::leaf("word", value)],
508 name,
509 ));
510 continue;
511 }
512 parts.words.push(Node::leaf("word", token.text));
513 }
514
515 parts
516 }
517
518 fn parse_simple_command(&mut self) -> Option<Node> {
519 let CommandParts {
520 mut assignments,
521 words,
522 redirects,
523 } = self.parse_command_parts();
524
525 if words.is_empty() {
526 if assignments.is_empty() {
527 return None;
528 }
529 return Some(if assignments.len() == 1 {
531 assignments.remove(0)
532 } else {
533 Node::new("block", assignments)
534 });
535 }
536
537 let name = words[0].text.clone().unwrap_or_default();
538 let rest: Vec<Node> = words[1..].to_vec();
539
540 match name.as_str() {
541 "export" | "local" | "readonly" => return Some(build_declaration(&name, rest)),
542 "set" if !rest.is_empty() => return Some(Node::new("set-option", rest)),
543 "exit" => return Some(Node::new("exit", rest)),
544 "return" => return Some(Node::new("return", rest)),
545 "source" | "." => return Some(Node::new("source", rest)),
546 _ => {}
547 }
548
549 let mut children = vec![
550 Node::new("word-list", words),
551 Node::new("redirect-list", redirects),
552 ];
553 if !assignments.is_empty() {
554 children.push(Node::new("assignment-list", assignments));
558 }
559 Some(Node::new("command", children))
560 }
561}
562
563fn build_declaration(keyword: &str, words: Vec<Node>) -> Node {
564 let term = if keyword == "export" {
565 "export"
566 } else {
567 "local"
568 };
569 let mut declarations: Vec<Node> = words
570 .into_iter()
571 .map(|word| {
572 let text = word.text.unwrap_or_default();
573 match match_assignment(&text) {
574 Some((name, value)) => Node::with_text(term, vec![Node::leaf("word", value)], name),
575 None => Node::with_text(term, vec![Node::leaf("word", "")], text),
576 }
577 })
578 .collect();
579
580 if declarations.len() == 1 {
581 declarations.remove(0)
582 } else {
583 Node::new("block", declarations)
584 }
585}
586
587pub fn parse_shell_script(source: &str) -> Node {
589 ShellParser::new(source).parse_script()
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595
596 fn terms(node: &Node) -> Vec<String> {
597 node.children
598 .iter()
599 .map(|child| child.term.clone())
600 .collect()
601 }
602
603 #[test]
604 fn parses_a_pipeline_into_stages() {
605 let script = parse_shell_script("ls | grep test | wc -l");
606 assert_eq!(script.term, "script");
607 assert_eq!(script.children[0].term, "pipeline");
608 assert_eq!(script.children[0].children.len(), 3);
609 }
610
611 #[test]
612 fn parses_and_or_left_associatively() {
613 let script = parse_shell_script("a && b || c");
614 assert_eq!(script.children[0].term, "or");
615 assert_eq!(script.children[0].children[0].term, "and");
616 }
617
618 #[test]
619 fn parses_control_flow_functions_and_case() {
620 let script = parse_shell_script(
621 "if [ -f x ]; then echo a; else echo b; fi\n\
622 while true; do echo loop; done\n\
623 for i in 1 2; do echo $i; done\n\
624 greet() { echo hi; }\n\
625 case \"$1\" in start) run ;; *) usage ;; esac\n",
626 );
627 assert_eq!(
628 terms(&script),
629 vec!["if", "while", "for", "function", "case"]
630 );
631 }
632
633 #[test]
634 fn reserved_words_stay_arguments_outside_command_position() {
635 let script = parse_shell_script("echo done | tr a-z A-Z");
636 let pipeline = &script.children[0];
637 assert_eq!(pipeline.term, "pipeline");
638 let words = &pipeline.children[0].children[0];
639 assert_eq!(words.children.len(), 2);
640 }
641}