rush_sh/parser/mod.rs
1//! Parser module for the Rush shell.
2//!
3//! This module provides functionality to parse tokenized shell input into an Abstract
4//! Syntax Tree (AST) that can be executed by the executor module.
5
6pub mod ast;
7mod control_flow;
8
9pub use ast::{Ast, Redirection, ShellCommand};
10use control_flow::*;
11
12use super::lexer::Token;
13
14/// Helper function to validate if a string is a valid variable name.
15/// Returns true if the name starts with a letter or underscore.
16fn is_valid_variable_name(name: &str) -> bool {
17 if let Some(first_char) = name.chars().next() {
18 first_char.is_alphabetic() || first_char == '_'
19 } else {
20 false
21 }
22}
23
24/// Helper function to create an empty body AST (a no-op that returns success).
25/// Used for empty then/else branches, empty loop bodies, and empty function bodies.
26pub(crate) fn create_empty_body_ast() -> Ast {
27 Ast::Pipeline(vec![ShellCommand {
28 args: vec!["true".to_string()],
29 redirections: Vec::new(),
30 compound: None,
31 }])
32}
33
34pub fn parse(tokens: Vec<Token>) -> Result<Ast, String> {
35 // First, try to detect and parse function definitions that span multiple lines
36 if tokens.len() >= 4
37 && let (Token::Word(_), Token::LeftParen, Token::RightParen, Token::LeftBrace) =
38 (&tokens[0], &tokens[1], &tokens[2], &tokens[3])
39 {
40 // Look for the matching RightBrace
41 // Start from the opening brace (token 3) and find its match
42 let mut brace_depth = 1; // We've already seen the opening brace at position 3
43 let mut function_end = tokens.len();
44 let mut j = 4; // Start after the opening brace
45
46 while j < tokens.len() {
47 match &tokens[j] {
48 Token::LeftBrace => {
49 brace_depth += 1;
50 j += 1;
51 }
52 Token::RightBrace => {
53 brace_depth -= 1;
54 if brace_depth == 0 {
55 function_end = j + 1; // Include the closing brace
56 break;
57 }
58 j += 1;
59 }
60 Token::If => {
61 // Skip to matching fi to avoid confusion
62 let mut if_depth = 1;
63 j += 1;
64 while j < tokens.len() && if_depth > 0 {
65 match tokens[j] {
66 Token::If => if_depth += 1,
67 Token::Fi => if_depth -= 1,
68 _ => {}
69 }
70 j += 1;
71 }
72 }
73 Token::For | Token::While | Token::Until => {
74 // Skip to matching done
75 let mut for_depth = 1;
76 j += 1;
77 while j < tokens.len() && for_depth > 0 {
78 match tokens[j] {
79 Token::For | Token::While | Token::Until => for_depth += 1,
80 Token::Done => for_depth -= 1,
81 _ => {}
82 }
83 j += 1;
84 }
85 }
86 Token::Case => {
87 // Skip to matching esac
88 j += 1;
89 while j < tokens.len() {
90 if tokens[j] == Token::Esac {
91 j += 1;
92 break;
93 }
94 j += 1;
95 }
96 }
97 _ => {
98 j += 1;
99 }
100 }
101 }
102
103 if brace_depth == 0 && function_end <= tokens.len() {
104 // We found the complete function definition
105 let function_tokens = &tokens[0..function_end];
106 let remaining_tokens = &tokens[function_end..];
107
108 let function_ast = parse_function_definition(function_tokens)?;
109
110 return if remaining_tokens.is_empty() {
111 Ok(function_ast)
112 } else {
113 // There are more commands after the function
114 let remaining_ast = parse_commands_sequentially(remaining_tokens)?;
115 Ok(Ast::Sequence(vec![function_ast, remaining_ast]))
116 };
117 }
118 }
119
120 // Also check for legacy function definition format (word with parentheses followed by brace)
121 if tokens.len() >= 2
122 && let Token::Word(ref word) = tokens[0]
123 && let Some(paren_pos) = word.find('(')
124 && word.ends_with(')')
125 && paren_pos > 0
126 && tokens[1] == Token::LeftBrace
127 {
128 return parse_function_definition(&tokens);
129 }
130
131 // Fall back to normal parsing
132 parse_commands_sequentially(&tokens)
133}
134
135/// Parses a single top-level command slice into an AST.
136///
137/// Recognizes assignments, local assignments, `return`, negation (`!`), control constructs
138/// (`if`, `case`, `for`, `while`, `until`), function definitions, and otherwise falls back to
139/// pipeline parsing to produce an `Ast` for the provided token slice.
140///
141/// # Returns
142///
143/// `Ok(Ast)` on success, `Err(String)` with a descriptive error message on failure (for example,
144/// when the slice is empty or a `!` is not followed by a command).
145///
146/// # Examples
147///
148/// ```
149/// // Note: parse_slice is a private function
150/// // This example is for documentation only
151/// ```
152fn parse_slice(tokens: &[Token]) -> Result<Ast, String> {
153 if tokens.is_empty() {
154 return Err("No commands found".to_string());
155 }
156
157 // Check if it's an assignment
158 if tokens.len() == 2 {
159 // Check for pattern: VAR= VALUE
160 if let (Token::Word(var_eq), Token::Word(value)) = (&tokens[0], &tokens[1])
161 && let Some(eq_pos) = var_eq.find('=')
162 && eq_pos > 0
163 && eq_pos < var_eq.len()
164 {
165 let var = var_eq[..eq_pos].to_string();
166 let full_value = format!("{}{}", &var_eq[eq_pos + 1..], value);
167 // Basic validation: variable name should start with letter or underscore
168 if is_valid_variable_name(&var) {
169 return Ok(Ast::Assignment {
170 var,
171 value: full_value,
172 });
173 }
174 }
175 }
176
177 // Check if it's an assignment (VAR= VALUE)
178 if tokens.len() == 2
179 && let (Token::Word(var_eq), Token::Word(value)) = (&tokens[0], &tokens[1])
180 && let Some(eq_pos) = var_eq.find('=')
181 && eq_pos > 0
182 && eq_pos == var_eq.len() - 1
183 {
184 let var = var_eq[..eq_pos].to_string();
185 // Basic validation: variable name should start with letter or underscore
186 if is_valid_variable_name(&var) {
187 return Ok(Ast::Assignment {
188 var,
189 value: value.clone(),
190 });
191 }
192 }
193
194 // Check if it's a local assignment (local VAR VALUE or local VAR= VALUE)
195 if tokens.len() == 3
196 && let (Token::Local, Token::Word(var), Token::Word(value)) =
197 (&tokens[0], &tokens[1], &tokens[2])
198 {
199 // Strip trailing = if present (handles "local var= value" format)
200 let clean_var = if var.ends_with('=') {
201 &var[..var.len() - 1]
202 } else {
203 var
204 };
205 // Basic validation: variable name should start with letter or underscore
206 if is_valid_variable_name(clean_var) {
207 return Ok(Ast::LocalAssignment {
208 var: clean_var.to_string(),
209 value: value.clone(),
210 });
211 } else {
212 return Err(format!("Invalid variable name: {}", clean_var));
213 }
214 }
215
216 // Check if it's a return statement
217 if !tokens.is_empty()
218 && tokens.len() <= 2
219 && let Token::Return = &tokens[0]
220 {
221 if tokens.len() == 1 {
222 // return (with no value, defaults to 0)
223 return Ok(Ast::Return { value: None });
224 } else if let Token::Word(word) = &tokens[1] {
225 // return value
226 return Ok(Ast::Return {
227 value: Some(word.clone()),
228 });
229 }
230 }
231
232 // Check if it's a local assignment (local VAR=VALUE)
233 if tokens.len() == 2
234 && let (Token::Local, Token::Word(var_eq)) = (&tokens[0], &tokens[1])
235 && let Some(eq_pos) = var_eq.find('=')
236 && eq_pos > 0
237 && eq_pos < var_eq.len()
238 {
239 let var = var_eq[..eq_pos].to_string();
240 let value = var_eq[eq_pos + 1..].to_string();
241 // Basic validation: variable name should start with letter or underscore
242 if is_valid_variable_name(&var) {
243 return Ok(Ast::LocalAssignment { var, value });
244 } else {
245 return Err(format!("Invalid variable name: {}", var));
246 }
247 }
248
249 // Check if it's a local assignment (local VAR) with no initial value
250 if tokens.len() == 2
251 && let (Token::Local, Token::Word(var)) = (&tokens[0], &tokens[1])
252 && !var.contains('=')
253 {
254 // Basic validation: variable name should start with letter or underscore
255 if is_valid_variable_name(var) {
256 return Ok(Ast::LocalAssignment {
257 var: var.clone(),
258 value: String::new(),
259 });
260 } else {
261 return Err(format!("Invalid variable name: {}", var));
262 }
263 }
264
265 // Check if it's an assignment (single token with =)
266 if tokens.len() == 1
267 && let Token::Word(ref word) = tokens[0]
268 && let Some(eq_pos) = word.find('=')
269 && eq_pos > 0
270 && eq_pos < word.len()
271 {
272 let var = word[..eq_pos].to_string();
273 let value = word[eq_pos + 1..].to_string();
274 // Basic validation: variable name should start with letter or underscore
275 if is_valid_variable_name(&var) {
276 return Ok(Ast::Assignment { var, value });
277 }
278 }
279
280 // Check if it's an if statement
281 if let Token::If = tokens[0] {
282 return parse_if(tokens);
283 }
284
285 // Check if it's a case statement
286 if let Token::Case = tokens[0] {
287 return parse_case(tokens);
288 }
289
290 // Check if it's a for loop
291 if let Token::For = tokens[0] {
292 return parse_for(tokens);
293 }
294
295 // Check if it's a while loop
296 if let Token::While = tokens[0] {
297 return parse_while(tokens);
298 }
299
300 // Check if it's an until loop
301 if let Token::Until = tokens[0] {
302 return parse_until(tokens);
303 }
304
305 // Check if it's a function definition
306 // Pattern: Word LeftParen RightParen LeftBrace
307 if tokens.len() >= 4
308 && let (Token::Word(word), Token::LeftParen, Token::RightParen, Token::LeftBrace) =
309 (&tokens[0], &tokens[1], &tokens[2], &tokens[3])
310 && is_valid_variable_name(word)
311 {
312 return parse_function_definition(tokens);
313 }
314
315 // Also check for function definition with parentheses in the word (legacy support)
316 if tokens.len() >= 2
317 && let Token::Word(ref word) = tokens[0]
318 && let Some(paren_pos) = word.find('(')
319 && word.ends_with(')')
320 && paren_pos > 0
321 {
322 let func_name = &word[..paren_pos];
323 if is_valid_variable_name(func_name) && tokens[1] == Token::LeftBrace {
324 return parse_function_definition(tokens);
325 }
326 }
327
328 // Check if it's a function call (word followed by arguments)
329 // For Phase 1, we'll parse as regular pipeline and handle function calls in executor
330
331 // Otherwise, parse as pipeline
332 parse_pipeline(tokens)
333}
334
335/// Helper function to parse a single command without operators.
336/// Returns the parsed AST and the number of tokens consumed.
337fn parse_single_command(tokens: &[Token]) -> Result<(Ast, usize), String> {
338 if tokens.is_empty() {
339 return Err("Expected command".to_string());
340 }
341
342 let mut i = 0;
343
344 // Skip leading newlines
345 while i < tokens.len() && tokens[i] == Token::Newline {
346 i += 1;
347 }
348
349 if i >= tokens.len() {
350 return Err("Expected command".to_string());
351 }
352
353 // Handle negation first - recursively parse what comes after !
354 if tokens[i] == Token::Bang {
355 i += 1; // Skip the bang
356
357 // Skip any newlines after the bang
358 while i < tokens.len() && tokens[i] == Token::Newline {
359 i += 1;
360 }
361
362 if i >= tokens.len() {
363 return Err("Expected command after !".to_string());
364 }
365
366 // Recursively parse the negated command
367 // IMPORTANT: This should only consume a single atomic command,
368 // not a chain with logical operators
369 let (negated_ast, consumed) = parse_single_command(&tokens[i..])?;
370 i += consumed;
371
372 // Negation only applies to the immediately following command
373 // Return immediately without consuming any following operators
374 return Ok((
375 Ast::Negation {
376 command: Box::new(negated_ast),
377 },
378 i,
379 ));
380 }
381
382 let start = i;
383
384 // Handle special constructs that have their own boundaries
385 match &tokens[i] {
386 Token::LeftParen => {
387 // Subshell - find matching paren
388 let mut paren_depth = 1;
389 i += 1;
390 while i < tokens.len() && paren_depth > 0 {
391 match tokens[i] {
392 Token::LeftParen => paren_depth += 1,
393 Token::RightParen => paren_depth -= 1,
394 _ => {}
395 }
396 i += 1;
397 }
398 if paren_depth != 0 {
399 return Err("Unmatched parenthesis".to_string());
400 }
401 }
402 Token::LeftBrace => {
403 // Command group - find matching brace
404 let mut brace_depth = 1;
405 i += 1;
406 while i < tokens.len() && brace_depth > 0 {
407 match tokens[i] {
408 Token::LeftBrace => brace_depth += 1,
409 Token::RightBrace => brace_depth -= 1,
410 _ => {}
411 }
412 i += 1;
413 }
414 if brace_depth != 0 {
415 return Err("Unmatched brace".to_string());
416 }
417 }
418 Token::If => {
419 // Find matching fi
420 let mut if_depth = 1;
421 i += 1;
422 while i < tokens.len() && if_depth > 0 {
423 match tokens[i] {
424 Token::If => if_depth += 1,
425 Token::Fi => {
426 if_depth -= 1;
427 if if_depth == 0 {
428 i += 1;
429 break;
430 }
431 }
432 _ => {}
433 }
434 i += 1;
435 }
436 }
437 Token::For | Token::While | Token::Until => {
438 // Find matching done
439 let mut loop_depth = 1;
440 i += 1;
441 while i < tokens.len() && loop_depth > 0 {
442 match tokens[i] {
443 Token::For | Token::While | Token::Until => loop_depth += 1,
444 Token::Done => {
445 loop_depth -= 1;
446 if loop_depth == 0 {
447 i += 1;
448 break;
449 }
450 }
451 _ => {}
452 }
453 i += 1;
454 }
455 }
456 Token::Case => {
457 // Find matching esac
458 i += 1;
459 while i < tokens.len() {
460 if tokens[i] == Token::Esac {
461 i += 1;
462 break;
463 }
464 i += 1;
465 }
466 }
467 _ => {
468 // Regular command/pipeline - stop at sequence separators
469 let mut brace_depth = 0;
470 let mut paren_depth = 0;
471 let mut last_was_pipe = false;
472
473 while i < tokens.len() {
474 // Check if we should stop before processing this token
475 // For logical operators (&&, ||), always stop at depth 0 after consuming at least one token
476 // For other separators, only stop after consuming at least one token
477 if i > start {
478 match &tokens[i] {
479 Token::And | Token::Or => {
480 if brace_depth == 0 && paren_depth == 0 {
481 if last_was_pipe {
482 return Err("Expected command after |".to_string());
483 }
484 break;
485 }
486 }
487 Token::Newline | Token::Semicolon | Token::Ampersand => {
488 if brace_depth == 0 && paren_depth == 0 && !last_was_pipe {
489 break;
490 }
491 }
492 Token::RightBrace if brace_depth == 0 => break,
493 Token::RightParen if paren_depth == 0 => break,
494 _ => {}
495 }
496 }
497
498 // Now process the token
499 match &tokens[i] {
500 Token::LeftBrace => {
501 brace_depth += 1;
502 last_was_pipe = false;
503 }
504 Token::RightBrace => {
505 if brace_depth > 0 {
506 brace_depth -= 1;
507 last_was_pipe = false;
508 }
509 }
510 Token::LeftParen => {
511 paren_depth += 1;
512 last_was_pipe = false;
513 }
514 Token::RightParen => {
515 if paren_depth > 0 {
516 paren_depth -= 1;
517 last_was_pipe = false;
518 }
519 }
520 Token::Pipe => last_was_pipe = true,
521 Token::Word(_) => last_was_pipe = false,
522 _ => last_was_pipe = false,
523 }
524 i += 1;
525 }
526 }
527 }
528
529 let command_tokens = &tokens[start..i];
530
531 // Safety check: ensure we consumed at least one token to prevent infinite loops
532 if i == start {
533 return Err("Internal parser error: parse_single_command consumed no tokens".to_string());
534 }
535
536 let mut ast = parse_slice(command_tokens)?;
537
538 // Check if this command should be executed asynchronously (ends with &)
539 if i < tokens.len() && tokens[i] == Token::Ampersand {
540 i += 1; // Consume the &
541 ast = Ast::AsyncCommand {
542 command: Box::new(ast),
543 };
544 }
545
546 Ok((ast, i))
547}
548
549/// Helper function to parse commands with && and || operators.
550/// This builds a left-associative chain of operators.
551/// Returns the parsed AST and the number of tokens consumed.
552fn parse_next_command(tokens: &[Token]) -> Result<(Ast, usize), String> {
553 // Parse the first command
554 let (mut ast, mut i) = parse_single_command(tokens)?;
555
556 // Build left-associative chain of && and || operators iteratively
557 loop {
558 // Check if there's an && or || operator after this command
559 if i >= tokens.len() || (tokens[i] != Token::And && tokens[i] != Token::Or) {
560 break;
561 }
562
563 let operator = tokens[i].clone();
564 i += 1; // Skip the operator
565
566 // Skip any newlines after the operator
567 while i < tokens.len() && tokens[i] == Token::Newline {
568 i += 1;
569 }
570
571 if i >= tokens.len() {
572 return Err("Expected command after operator".to_string());
573 }
574
575 // Parse the next single command (without chaining)
576 let (right_ast, consumed) = parse_single_command(&tokens[i..])?;
577 i += consumed;
578
579 // Build left-associative structure
580 ast = match operator {
581 Token::And => Ast::And {
582 left: Box::new(ast),
583 right: Box::new(right_ast),
584 },
585 Token::Or => Ast::Or {
586 left: Box::new(ast),
587 right: Box::new(right_ast),
588 },
589 _ => unreachable!(),
590 };
591 }
592
593 Ok((ast, i))
594}
595
596/// Parses a slice of tokens into a top-level AST representing one or more sequential shell commands.
597///
598/// This function consumes the provided token sequence and produces an `Ast` that represents either a
599/// single command/pipeline/compound construct or a `Sequence` of commands joined by semicolons/newlines
600/// and conditional operators. It recognizes subshells, command groups, pipelines, redirections,
601/// negation (`!`), function definitions, and control-flow blocks, and composes appropriate AST nodes.
602///
603/// # Errors
604///
605/// Returns an `Err(String)` when the tokens contain a syntactic problem that prevents building a valid AST,
606/// for example unmatched braces/parentheses, an empty subshell or command group, or when no commands are present.
607///
608/// # Examples
609///
610/// ```
611/// // Note: parse_commands_sequentially is a private function
612/// // This example is for documentation only
613/// ```
614fn parse_commands_sequentially(tokens: &[Token]) -> Result<Ast, String> {
615 let mut i = 0;
616 let mut commands = Vec::new();
617
618 while i < tokens.len() {
619 // Skip whitespace and comments
620 while i < tokens.len() {
621 match &tokens[i] {
622 Token::Newline => {
623 i += 1;
624 }
625 Token::Word(word) if word.starts_with('#') => {
626 // Skip comment line
627 while i < tokens.len() && tokens[i] != Token::Newline {
628 i += 1;
629 }
630 if i < tokens.len() {
631 i += 1; // Skip the newline
632 }
633 }
634 _ => break,
635 }
636 }
637
638 if i >= tokens.len() {
639 break;
640 }
641
642 // Find the end of this command
643 let start = i;
644
645 // Check for subshell: LeftParen at start of command
646 // Must check BEFORE function definition to avoid ambiguity
647 if tokens[i] == Token::LeftParen {
648 // This is a subshell - find the matching RightParen
649 let mut paren_depth = 1;
650 let mut j = i + 1;
651
652 while j < tokens.len() && paren_depth > 0 {
653 match tokens[j] {
654 Token::LeftParen => paren_depth += 1,
655 Token::RightParen => paren_depth -= 1,
656 _ => {}
657 }
658 j += 1;
659 }
660
661 if paren_depth != 0 {
662 return Err("Unmatched parenthesis in subshell".to_string());
663 }
664
665 // Extract subshell body (tokens between parens)
666 let subshell_tokens = &tokens[i + 1..j - 1];
667
668 // Parse the subshell body recursively
669 // Empty subshells are not allowed
670 let body_ast = if subshell_tokens.is_empty() {
671 return Err("Empty subshell".to_string());
672 } else {
673 parse_commands_sequentially(subshell_tokens)?
674 };
675
676 let mut subshell_ast = Ast::Subshell {
677 body: Box::new(body_ast),
678 };
679
680 i = j; // Move past the closing paren
681
682 // Check for redirections after subshell
683 let mut redirections = Vec::new();
684 while i < tokens.len() {
685 match &tokens[i] {
686 Token::RedirOut => {
687 i += 1;
688 if i < tokens.len()
689 && let Token::Word(file) = &tokens[i]
690 {
691 redirections.push(Redirection::Output(file.clone()));
692 i += 1;
693 }
694 }
695 Token::RedirOutClobber => {
696 i += 1;
697 if i >= tokens.len() {
698 return Err("expected filename after >|".to_string());
699 }
700 if let Token::Word(file) = &tokens[i] {
701 redirections.push(Redirection::OutputClobber(file.clone()));
702 i += 1;
703 } else {
704 return Err("expected filename after >|".to_string());
705 }
706 }
707 Token::RedirIn => {
708 i += 1;
709 if i < tokens.len()
710 && let Token::Word(file) = &tokens[i]
711 {
712 redirections.push(Redirection::Input(file.clone()));
713 i += 1;
714 }
715 }
716 Token::RedirAppend => {
717 i += 1;
718 if i < tokens.len()
719 && let Token::Word(file) = &tokens[i]
720 {
721 redirections.push(Redirection::Append(file.clone()));
722 i += 1;
723 }
724 }
725 Token::RedirectFdOut(fd, file) => {
726 redirections.push(Redirection::FdOutput(*fd, file.clone()));
727 i += 1;
728 }
729 Token::RedirectFdOutClobber(fd, file) => {
730 redirections.push(Redirection::FdOutputClobber(*fd, file.clone()));
731 i += 1;
732 }
733 Token::RedirectFdIn(fd, file) => {
734 redirections.push(Redirection::FdInput(*fd, file.clone()));
735 i += 1;
736 }
737 Token::RedirectFdAppend(fd, file) => {
738 redirections.push(Redirection::FdAppend(*fd, file.clone()));
739 i += 1;
740 }
741 Token::RedirectFdDup(from_fd, to_fd) => {
742 redirections.push(Redirection::FdDuplicate(*from_fd, *to_fd));
743 i += 1;
744 }
745 Token::RedirectFdClose(fd) => {
746 redirections.push(Redirection::FdClose(*fd));
747 i += 1;
748 }
749 Token::RedirectFdInOut(fd, file) => {
750 redirections.push(Redirection::FdInputOutput(*fd, file.clone()));
751 i += 1;
752 }
753 Token::RedirHereDoc(delimiter, quoted) => {
754 redirections
755 .push(Redirection::HereDoc(delimiter.clone(), quoted.to_string()));
756 i += 1;
757 }
758 Token::RedirHereString(content) => {
759 redirections.push(Redirection::HereString(content.clone()));
760 i += 1;
761 }
762 _ => break,
763 }
764 }
765
766 // Check if this subshell is part of a pipeline
767 if i < tokens.len() && tokens[i] == Token::Pipe {
768 // Find end of pipeline
769 let mut end = i;
770 let mut brace_depth = 0;
771 let mut paren_depth = 0;
772 let mut last_was_pipe = true; // Started with a pipe
773 while end < tokens.len() {
774 match &tokens[end] {
775 Token::Pipe => last_was_pipe = true,
776 Token::LeftBrace => {
777 brace_depth += 1;
778 last_was_pipe = false;
779 }
780 Token::RightBrace => {
781 if brace_depth > 0 {
782 brace_depth -= 1;
783 } else {
784 break;
785 }
786 last_was_pipe = false;
787 }
788 Token::LeftParen => {
789 paren_depth += 1;
790 last_was_pipe = false;
791 }
792 Token::RightParen => {
793 if paren_depth > 0 {
794 paren_depth -= 1;
795 } else {
796 break;
797 }
798 last_was_pipe = false;
799 }
800 Token::Newline | Token::Semicolon => {
801 if brace_depth == 0 && paren_depth == 0 && !last_was_pipe {
802 break;
803 }
804 }
805 Token::Word(_) => last_was_pipe = false,
806 _ => {}
807 }
808 end += 1;
809 }
810
811 let pipeline_ast = parse_pipeline(&tokens[start..end])?;
812 commands.push(pipeline_ast);
813 i = end;
814 continue;
815 }
816
817 // If not part of a pipeline, apply redirections to the subshell itself
818 if !redirections.is_empty() {
819 subshell_ast = Ast::Pipeline(vec![ShellCommand {
820 args: Vec::new(),
821 redirections,
822 compound: Some(Box::new(subshell_ast)),
823 }]);
824 }
825
826 // Check if this subshell should be executed asynchronously (ends with &)
827 if i < tokens.len() && tokens[i] == Token::Ampersand {
828 i += 1; // Consume the &
829 subshell_ast = Ast::AsyncCommand {
830 command: Box::new(subshell_ast),
831 };
832 commands.push(subshell_ast);
833
834 // Skip semicolon or newline after async subshell
835 if i < tokens.len() && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon) {
836 i += 1;
837 }
838 continue;
839 }
840
841 // Handle operators after subshell (&&, ||, ;, newline)
842 if i < tokens.len() && (tokens[i] == Token::And || tokens[i] == Token::Or) {
843 let operator = tokens[i].clone();
844 i += 1; // Skip the operator
845
846 // Skip any newlines after the operator
847 while i < tokens.len() && tokens[i] == Token::Newline {
848 i += 1;
849 }
850
851 // Parse only the next command (not the entire remaining sequence)
852 let (right_ast, consumed) = parse_next_command(&tokens[i..])?;
853 i += consumed;
854
855 // Create And or Or node
856 let combined_ast = match operator {
857 Token::And => Ast::And {
858 left: Box::new(subshell_ast),
859 right: Box::new(right_ast),
860 },
861 Token::Or => Ast::Or {
862 left: Box::new(subshell_ast),
863 right: Box::new(right_ast),
864 },
865 _ => unreachable!(),
866 };
867
868 commands.push(combined_ast);
869
870 // Skip semicolon or newline after the combined command
871 if i < tokens.len()
872 && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon)
873 {
874 i += 1;
875 }
876 continue;
877 } else {
878 commands.push(subshell_ast);
879 }
880
881 // Skip semicolon or newline after subshell
882 if i < tokens.len() && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon) {
883 i += 1;
884 }
885 continue;
886 }
887
888 // Check for command group: LeftBrace at start of command
889 if tokens[i] == Token::LeftBrace {
890 // This is a command group - find the matching RightBrace
891 let mut brace_depth = 1;
892 let mut j = i + 1;
893
894 while j < tokens.len() && brace_depth > 0 {
895 match tokens[j] {
896 Token::LeftBrace => brace_depth += 1,
897 Token::RightBrace => brace_depth -= 1,
898 _ => {}
899 }
900 j += 1;
901 }
902
903 if brace_depth != 0 {
904 return Err("Unmatched brace in command group".to_string());
905 }
906
907 // Extract group body (tokens between braces)
908 let group_tokens = &tokens[i + 1..j - 1];
909
910 // Parse the group body recursively
911 // Empty groups are not allowed
912 let body_ast = if group_tokens.is_empty() {
913 return Err("Empty command group".to_string());
914 } else {
915 parse_commands_sequentially(group_tokens)?
916 };
917
918 let mut group_ast = Ast::CommandGroup {
919 body: Box::new(body_ast),
920 };
921
922 i = j; // Move past the closing brace
923
924 // Check for redirections after command group
925 let mut redirections = Vec::new();
926 while i < tokens.len() {
927 match &tokens[i] {
928 Token::RedirOut => {
929 i += 1;
930 if i < tokens.len()
931 && let Token::Word(file) = &tokens[i]
932 {
933 redirections.push(Redirection::Output(file.clone()));
934 i += 1;
935 }
936 }
937 Token::RedirOutClobber => {
938 i += 1;
939 if i >= tokens.len() {
940 return Err("expected filename after >|".to_string());
941 }
942 if let Token::Word(file) = &tokens[i] {
943 redirections.push(Redirection::OutputClobber(file.clone()));
944 i += 1;
945 } else {
946 return Err("expected filename after >|".to_string());
947 }
948 }
949 Token::RedirIn => {
950 i += 1;
951 if i < tokens.len()
952 && let Token::Word(file) = &tokens[i]
953 {
954 redirections.push(Redirection::Input(file.clone()));
955 i += 1;
956 }
957 }
958 Token::RedirAppend => {
959 i += 1;
960 if i < tokens.len()
961 && let Token::Word(file) = &tokens[i]
962 {
963 redirections.push(Redirection::Append(file.clone()));
964 i += 1;
965 }
966 }
967 Token::RedirectFdOut(fd, file) => {
968 redirections.push(Redirection::FdOutput(*fd, file.clone()));
969 i += 1;
970 }
971 Token::RedirectFdIn(fd, file) => {
972 redirections.push(Redirection::FdInput(*fd, file.clone()));
973 i += 1;
974 }
975 Token::RedirectFdAppend(fd, file) => {
976 redirections.push(Redirection::FdAppend(*fd, file.clone()));
977 i += 1;
978 }
979 Token::RedirectFdDup(from_fd, to_fd) => {
980 redirections.push(Redirection::FdDuplicate(*from_fd, *to_fd));
981 i += 1;
982 }
983 Token::RedirectFdClose(fd) => {
984 redirections.push(Redirection::FdClose(*fd));
985 i += 1;
986 }
987 Token::RedirectFdInOut(fd, file) => {
988 redirections.push(Redirection::FdInputOutput(*fd, file.clone()));
989 i += 1;
990 }
991 Token::RedirHereDoc(delimiter, quoted) => {
992 redirections
993 .push(Redirection::HereDoc(delimiter.clone(), quoted.to_string()));
994 i += 1;
995 }
996 Token::RedirHereString(content) => {
997 redirections.push(Redirection::HereString(content.clone()));
998 i += 1;
999 }
1000 _ => break,
1001 }
1002 }
1003
1004 // Check if this group is part of a pipeline
1005 if i < tokens.len() && tokens[i] == Token::Pipe {
1006 // Find end of pipeline
1007 let mut end = i;
1008 let mut brace_depth = 0;
1009 let mut paren_depth = 0;
1010 let mut last_was_pipe = true; // Started with a pipe
1011 while end < tokens.len() {
1012 match &tokens[end] {
1013 Token::Pipe => last_was_pipe = true,
1014 Token::LeftBrace => {
1015 brace_depth += 1;
1016 last_was_pipe = false;
1017 }
1018 Token::RightBrace => {
1019 if brace_depth > 0 {
1020 brace_depth -= 1;
1021 } else {
1022 break;
1023 }
1024 last_was_pipe = false;
1025 }
1026 Token::LeftParen => {
1027 paren_depth += 1;
1028 last_was_pipe = false;
1029 }
1030 Token::RightParen => {
1031 if paren_depth > 0 {
1032 paren_depth -= 1;
1033 } else {
1034 break;
1035 }
1036 last_was_pipe = false;
1037 }
1038 Token::Newline | Token::Semicolon => {
1039 if brace_depth == 0 && paren_depth == 0 && !last_was_pipe {
1040 break;
1041 }
1042 }
1043 Token::Word(_) => last_was_pipe = false,
1044 _ => {}
1045 }
1046 end += 1;
1047 }
1048
1049 let pipeline_ast = parse_pipeline(&tokens[start..end])?;
1050 commands.push(pipeline_ast);
1051 i = end;
1052 continue;
1053 }
1054
1055 // If not part of a pipeline, apply redirections to the group itself
1056 if !redirections.is_empty() {
1057 group_ast = Ast::Pipeline(vec![ShellCommand {
1058 args: Vec::new(),
1059 redirections,
1060 compound: Some(Box::new(group_ast)),
1061 }]);
1062 }
1063
1064 // Check if this command group should be executed asynchronously (ends with &)
1065 if i < tokens.len() && tokens[i] == Token::Ampersand {
1066 i += 1; // Consume the &
1067 group_ast = Ast::AsyncCommand {
1068 command: Box::new(group_ast),
1069 };
1070 commands.push(group_ast);
1071
1072 // Skip semicolon or newline after async command group
1073 if i < tokens.len() && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon) {
1074 i += 1;
1075 }
1076 continue;
1077 }
1078
1079 // Handle operators after group (&&, ||, ;, newline)
1080 if i < tokens.len() && (tokens[i] == Token::And || tokens[i] == Token::Or) {
1081 let operator = tokens[i].clone();
1082 i += 1; // Skip the operator
1083
1084 // Skip any newlines after the operator
1085 while i < tokens.len() && tokens[i] == Token::Newline {
1086 i += 1;
1087 }
1088
1089 // Parse only the next command (not the entire remaining sequence)
1090 let (right_ast, consumed) = parse_next_command(&tokens[i..])?;
1091 i += consumed;
1092
1093 // Create And or Or node
1094 let combined_ast = match operator {
1095 Token::And => Ast::And {
1096 left: Box::new(group_ast),
1097 right: Box::new(right_ast),
1098 },
1099 Token::Or => Ast::Or {
1100 left: Box::new(group_ast),
1101 right: Box::new(right_ast),
1102 },
1103 _ => unreachable!(),
1104 };
1105
1106 commands.push(combined_ast);
1107
1108 // Skip semicolon or newline after the combined command
1109 if i < tokens.len()
1110 && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon)
1111 {
1112 i += 1;
1113 }
1114 continue;
1115 } else {
1116 commands.push(group_ast);
1117 }
1118
1119 // Skip semicolon or newline after group
1120 if i < tokens.len() && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon) {
1121 i += 1;
1122 }
1123 continue;
1124 }
1125
1126 // Special handling for compound commands
1127 if tokens[i] == Token::If {
1128 // For if statements, find the matching fi
1129 let mut depth = 0;
1130 while i < tokens.len() {
1131 match tokens[i] {
1132 Token::If => depth += 1,
1133 Token::Fi => {
1134 depth -= 1;
1135 if depth == 0 {
1136 i += 1; // Include the fi
1137 break;
1138 }
1139 }
1140 _ => {}
1141 }
1142 i += 1;
1143 }
1144
1145 // If we didn't find a matching fi, include all remaining tokens
1146 // This handles the case where the if statement is incomplete
1147 } else if tokens[i] == Token::For {
1148 // For for loops, find the matching done
1149 let mut depth = 1; // Start at 1 because we're already inside the for
1150 i += 1; // Move past the 'for' token
1151 while i < tokens.len() {
1152 match tokens[i] {
1153 Token::For | Token::While | Token::Until => depth += 1,
1154 Token::Done => {
1155 depth -= 1;
1156 if depth == 0 {
1157 i += 1; // Include the done
1158 break;
1159 }
1160 }
1161 _ => {}
1162 }
1163 i += 1;
1164 }
1165 } else if tokens[i] == Token::While {
1166 // For while loops, find the matching done
1167 let mut depth = 1; // Start at 1 because we're already inside the while
1168 i += 1; // Move past the 'while' token
1169 while i < tokens.len() {
1170 match tokens[i] {
1171 Token::While | Token::For | Token::Until => depth += 1,
1172 Token::Done => {
1173 depth -= 1;
1174 if depth == 0 {
1175 i += 1; // Include the done
1176 break;
1177 }
1178 }
1179 _ => {}
1180 }
1181 i += 1;
1182 }
1183 } else if tokens[i] == Token::Until {
1184 // For until loops, find the matching done
1185 let mut depth = 1; // Start at 1 because we're already inside the until
1186 i += 1; // Move past the 'until' token
1187 while i < tokens.len() {
1188 match tokens[i] {
1189 Token::Until | Token::For | Token::While => depth += 1,
1190 Token::Done => {
1191 depth -= 1;
1192 if depth == 0 {
1193 i += 1; // Include the done
1194 break;
1195 }
1196 }
1197 _ => {}
1198 }
1199 i += 1;
1200 }
1201 } else if tokens[i] == Token::Case {
1202 // For case statements, find the matching esac
1203 while i < tokens.len() {
1204 if tokens[i] == Token::Esac {
1205 i += 1; // Include the esac
1206 break;
1207 }
1208 i += 1;
1209 }
1210 } else if i + 3 < tokens.len()
1211 && matches!(tokens[i], Token::Word(_))
1212 && tokens[i + 1] == Token::LeftParen
1213 && tokens[i + 2] == Token::RightParen
1214 && tokens[i + 3] == Token::LeftBrace
1215 {
1216 // This is a function definition - find the matching closing brace
1217 let mut brace_depth = 1;
1218 i += 4; // Skip to after opening brace
1219 while i < tokens.len() && brace_depth > 0 {
1220 match tokens[i] {
1221 Token::LeftBrace => brace_depth += 1,
1222 Token::RightBrace => brace_depth -= 1,
1223 _ => {}
1224 }
1225 i += 1;
1226 }
1227 } else {
1228 // Sanity check: we shouldn't be starting a command with an operator
1229 if matches!(tokens[i], Token::And | Token::Or | Token::Semicolon) {
1230 return Err(format!(
1231 "Unexpected operator at command start: {:?}",
1232 tokens[i]
1233 ));
1234 }
1235
1236 // For simple commands, stop at newline, semicolon, &&, or ||
1237 // But check if the next token after newline is a control flow keyword
1238 let mut brace_depth = 0;
1239 let mut paren_depth = 0;
1240 let mut last_was_pipe = false;
1241 while i < tokens.len() {
1242 match &tokens[i] {
1243 Token::LeftBrace => {
1244 brace_depth += 1;
1245 last_was_pipe = false;
1246 }
1247 Token::RightBrace => {
1248 if brace_depth > 0 {
1249 brace_depth -= 1;
1250 } else {
1251 break;
1252 }
1253 last_was_pipe = false;
1254 }
1255 Token::LeftParen => {
1256 paren_depth += 1;
1257 last_was_pipe = false;
1258 }
1259 Token::RightParen => {
1260 if paren_depth > 0 {
1261 paren_depth -= 1;
1262 } else {
1263 break;
1264 }
1265 last_was_pipe = false;
1266 }
1267 Token::Pipe => last_was_pipe = true,
1268 Token::Newline | Token::Semicolon | Token::And | Token::Or | Token::Ampersand => {
1269 if brace_depth == 0 && paren_depth == 0 && !last_was_pipe {
1270 break;
1271 }
1272 }
1273 Token::Word(_) => last_was_pipe = false,
1274 _ => {}
1275 }
1276 i += 1;
1277 }
1278 }
1279
1280 let command_tokens = &tokens[start..i];
1281 if !command_tokens.is_empty() {
1282 // Don't try to parse orphaned else/elif/fi tokens
1283 if command_tokens.len() == 1 {
1284 match command_tokens[0] {
1285 Token::Else | Token::Elif | Token::Fi => {
1286 // Skip orphaned control flow tokens
1287 if i < tokens.len()
1288 && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon)
1289 {
1290 i += 1;
1291 }
1292 continue;
1293 }
1294 _ => {}
1295 }
1296 }
1297
1298 // Use parse_next_command to handle operators
1299 let (mut ast, consumed) = parse_next_command(&tokens[start..])?;
1300 i = start + consumed;
1301
1302 // Check if this command should be executed asynchronously (ends with &)
1303 if i < tokens.len() && tokens[i] == Token::Ampersand {
1304 i += 1; // Consume the &
1305 ast = Ast::AsyncCommand {
1306 command: Box::new(ast),
1307 };
1308 }
1309
1310 commands.push(ast);
1311 }
1312
1313 if i < tokens.len() && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon) {
1314 i += 1;
1315 }
1316 }
1317
1318 if commands.is_empty() {
1319 return Err("No commands found".to_string());
1320 }
1321
1322 if commands.len() == 1 {
1323 Ok(commands.into_iter().next().unwrap())
1324 } else {
1325 Ok(Ast::Sequence(commands))
1326 }
1327}
1328
1329/// Parses a sequence of tokens into an `Ast::Pipeline` representing one or more pipeline stages.
1330///
1331/// The resulting pipeline contains one `ShellCommand` per stage with collected `args`,
1332/// ordered `redirections`, and an optional `compound` (subshell or command group). Returns an
1333/// error if the tokens contain unmatched braces/parentheses, an unexpected token, or no commands.
1334///
1335/// # Examples
1336///
1337/// ```
1338/// // Note: parse_pipeline is a private function
1339/// // This example is for documentation only
1340/// ```
1341fn parse_pipeline(tokens: &[Token]) -> Result<Ast, String> {
1342 let mut commands = Vec::new();
1343 let mut current_cmd = ShellCommand::default();
1344
1345 let mut i = 0;
1346 while i < tokens.len() {
1347 let token = &tokens[i];
1348 match token {
1349 Token::LeftBrace => {
1350 // Start of command group in pipeline
1351 // Find matching RightBrace
1352 let mut brace_depth = 1;
1353 let mut j = i + 1;
1354
1355 while j < tokens.len() && brace_depth > 0 {
1356 match tokens[j] {
1357 Token::LeftBrace => brace_depth += 1,
1358 Token::RightBrace => brace_depth -= 1,
1359 _ => {}
1360 }
1361 j += 1;
1362 }
1363
1364 if brace_depth != 0 {
1365 return Err("Unmatched brace in pipeline".to_string());
1366 }
1367
1368 // Parse group body
1369 let group_tokens = &tokens[i + 1..j - 1];
1370
1371 // Empty groups are valid and equivalent to 'true'
1372 let body_ast = if group_tokens.is_empty() {
1373 create_empty_body_ast()
1374 } else {
1375 parse_commands_sequentially(group_tokens)?
1376 };
1377
1378 // Create ShellCommand with compound command group
1379 current_cmd.compound = Some(Box::new(Ast::CommandGroup {
1380 body: Box::new(body_ast),
1381 }));
1382
1383 i = j; // Move past closing brace
1384
1385 // Check for redirections after command group
1386 while i < tokens.len() {
1387 match &tokens[i] {
1388 Token::RedirOut => {
1389 i += 1;
1390 if i < tokens.len()
1391 && let Token::Word(file) = &tokens[i]
1392 {
1393 current_cmd
1394 .redirections
1395 .push(Redirection::Output(file.clone()));
1396 i += 1;
1397 }
1398 }
1399 Token::RedirOutClobber => {
1400 i += 1;
1401 if i >= tokens.len() {
1402 return Err("expected filename after >|".to_string());
1403 }
1404 if let Token::Word(file) = &tokens[i] {
1405 current_cmd
1406 .redirections
1407 .push(Redirection::OutputClobber(file.clone()));
1408 i += 1;
1409 } else {
1410 return Err("expected filename after >|".to_string());
1411 }
1412 }
1413 Token::RedirIn => {
1414 i += 1;
1415 if i < tokens.len()
1416 && let Token::Word(file) = &tokens[i]
1417 {
1418 current_cmd
1419 .redirections
1420 .push(Redirection::Input(file.clone()));
1421 i += 1;
1422 }
1423 }
1424 Token::RedirAppend => {
1425 i += 1;
1426 if i < tokens.len()
1427 && let Token::Word(file) = &tokens[i]
1428 {
1429 current_cmd
1430 .redirections
1431 .push(Redirection::Append(file.clone()));
1432 i += 1;
1433 }
1434 }
1435 Token::RedirectFdOut(fd, file) => {
1436 current_cmd
1437 .redirections
1438 .push(Redirection::FdOutput(*fd, file.clone()));
1439 i += 1;
1440 }
1441 Token::RedirectFdOutClobber(fd, file) => {
1442 current_cmd
1443 .redirections
1444 .push(Redirection::FdOutputClobber(*fd, file.clone()));
1445 i += 1;
1446 }
1447 Token::RedirectFdIn(fd, file) => {
1448 current_cmd
1449 .redirections
1450 .push(Redirection::FdInput(*fd, file.clone()));
1451 i += 1;
1452 }
1453 Token::RedirectFdAppend(fd, file) => {
1454 current_cmd
1455 .redirections
1456 .push(Redirection::FdAppend(*fd, file.clone()));
1457 i += 1;
1458 }
1459 Token::RedirectFdDup(from_fd, to_fd) => {
1460 current_cmd
1461 .redirections
1462 .push(Redirection::FdDuplicate(*from_fd, *to_fd));
1463 i += 1;
1464 }
1465 Token::RedirectFdClose(fd) => {
1466 current_cmd.redirections.push(Redirection::FdClose(*fd));
1467 i += 1;
1468 }
1469 Token::RedirectFdInOut(fd, file) => {
1470 current_cmd
1471 .redirections
1472 .push(Redirection::FdInputOutput(*fd, file.clone()));
1473 i += 1;
1474 }
1475 Token::RedirHereDoc(delimiter, quoted) => {
1476 current_cmd
1477 .redirections
1478 .push(Redirection::HereDoc(delimiter.clone(), quoted.to_string()));
1479 i += 1;
1480 }
1481 Token::RedirHereString(content) => {
1482 current_cmd
1483 .redirections
1484 .push(Redirection::HereString(content.clone()));
1485 i += 1;
1486 }
1487 Token::Pipe => {
1488 // End of this pipeline stage
1489 break;
1490 }
1491 _ => break,
1492 }
1493 }
1494
1495 // Stage will be pushed at next | or end of loop
1496 continue;
1497 }
1498 Token::LeftParen => {
1499 // Start of subshell in pipeline
1500 // Find matching RightParen
1501 let mut paren_depth = 1;
1502 let mut j = i + 1;
1503
1504 while j < tokens.len() && paren_depth > 0 {
1505 match tokens[j] {
1506 Token::LeftParen => paren_depth += 1,
1507 Token::RightParen => paren_depth -= 1,
1508 _ => {}
1509 }
1510 j += 1;
1511 }
1512
1513 if paren_depth != 0 {
1514 return Err("Unmatched parenthesis in pipeline".to_string());
1515 }
1516
1517 // Parse subshell body
1518 let subshell_tokens = &tokens[i + 1..j - 1];
1519
1520 // Empty subshells are valid and equivalent to 'true'
1521 let body_ast = if subshell_tokens.is_empty() {
1522 create_empty_body_ast()
1523 } else {
1524 parse_commands_sequentially(subshell_tokens)?
1525 };
1526
1527 // Create ShellCommand with compound subshell
1528 // Create ShellCommand with compound subshell
1529 current_cmd.compound = Some(Box::new(Ast::Subshell {
1530 body: Box::new(body_ast),
1531 }));
1532
1533 i = j; // Move past closing paren
1534
1535 // Check for redirections after subshell
1536 while i < tokens.len() {
1537 match &tokens[i] {
1538 Token::RedirOut => {
1539 i += 1;
1540 if i < tokens.len()
1541 && let Token::Word(file) = &tokens[i]
1542 {
1543 current_cmd
1544 .redirections
1545 .push(Redirection::Output(file.clone()));
1546 i += 1;
1547 }
1548 }
1549 Token::RedirOutClobber => {
1550 i += 1;
1551 if i >= tokens.len() {
1552 return Err("expected filename after >|".to_string());
1553 }
1554 if let Token::Word(file) = &tokens[i] {
1555 current_cmd
1556 .redirections
1557 .push(Redirection::OutputClobber(file.clone()));
1558 i += 1;
1559 } else {
1560 return Err("expected filename after >|".to_string());
1561 }
1562 }
1563 Token::RedirIn => {
1564 i += 1;
1565 if i < tokens.len()
1566 && let Token::Word(file) = &tokens[i]
1567 {
1568 current_cmd
1569 .redirections
1570 .push(Redirection::Input(file.clone()));
1571 i += 1;
1572 }
1573 }
1574 Token::RedirAppend => {
1575 i += 1;
1576 if i < tokens.len()
1577 && let Token::Word(file) = &tokens[i]
1578 {
1579 current_cmd
1580 .redirections
1581 .push(Redirection::Append(file.clone()));
1582 i += 1;
1583 }
1584 }
1585 Token::RedirectFdOut(fd, file) => {
1586 current_cmd
1587 .redirections
1588 .push(Redirection::FdOutput(*fd, file.clone()));
1589 i += 1;
1590 }
1591 Token::RedirectFdOutClobber(fd, file) => {
1592 current_cmd
1593 .redirections
1594 .push(Redirection::FdOutputClobber(*fd, file.clone()));
1595 i += 1;
1596 }
1597 Token::RedirectFdIn(fd, file) => {
1598 current_cmd
1599 .redirections
1600 .push(Redirection::FdInput(*fd, file.clone()));
1601 i += 1;
1602 }
1603 Token::RedirectFdAppend(fd, file) => {
1604 current_cmd
1605 .redirections
1606 .push(Redirection::FdAppend(*fd, file.clone()));
1607 i += 1;
1608 }
1609 Token::RedirectFdDup(from_fd, to_fd) => {
1610 current_cmd
1611 .redirections
1612 .push(Redirection::FdDuplicate(*from_fd, *to_fd));
1613 i += 1;
1614 }
1615 Token::RedirectFdClose(fd) => {
1616 current_cmd.redirections.push(Redirection::FdClose(*fd));
1617 i += 1;
1618 }
1619 Token::RedirectFdInOut(fd, file) => {
1620 current_cmd
1621 .redirections
1622 .push(Redirection::FdInputOutput(*fd, file.clone()));
1623 i += 1;
1624 }
1625 Token::RedirHereDoc(delimiter, quoted) => {
1626 current_cmd
1627 .redirections
1628 .push(Redirection::HereDoc(delimiter.clone(), quoted.to_string()));
1629 i += 1;
1630 }
1631 Token::RedirHereString(content) => {
1632 current_cmd
1633 .redirections
1634 .push(Redirection::HereString(content.clone()));
1635 i += 1;
1636 }
1637 Token::Pipe => {
1638 // End of this pipeline stage
1639 break;
1640 }
1641 _ => break,
1642 }
1643 }
1644
1645 // Stage will be pushed at next | or end of loop
1646 continue;
1647 }
1648 Token::Word(word) => {
1649 current_cmd.args.push(word.clone());
1650 }
1651 Token::Local => {
1652 current_cmd.args.push("local".to_string());
1653 }
1654 Token::Return => {
1655 current_cmd.args.push("return".to_string());
1656 }
1657 Token::Break => {
1658 current_cmd.args.push("break".to_string());
1659 }
1660 Token::Continue => {
1661 current_cmd.args.push("continue".to_string());
1662 }
1663 // Handle keywords as command arguments
1664 // When keywords appear in pipeline context (not at start of command),
1665 // they should be treated as regular word arguments
1666 Token::If => {
1667 current_cmd.args.push("if".to_string());
1668 }
1669 Token::Then => {
1670 current_cmd.args.push("then".to_string());
1671 }
1672 Token::Else => {
1673 current_cmd.args.push("else".to_string());
1674 }
1675 Token::Elif => {
1676 current_cmd.args.push("elif".to_string());
1677 }
1678 Token::Fi => {
1679 current_cmd.args.push("fi".to_string());
1680 }
1681 Token::Case => {
1682 current_cmd.args.push("case".to_string());
1683 }
1684 Token::In => {
1685 current_cmd.args.push("in".to_string());
1686 }
1687 Token::Esac => {
1688 current_cmd.args.push("esac".to_string());
1689 }
1690 Token::For => {
1691 current_cmd.args.push("for".to_string());
1692 }
1693 Token::While => {
1694 current_cmd.args.push("while".to_string());
1695 }
1696 Token::Until => {
1697 current_cmd.args.push("until".to_string());
1698 }
1699 Token::Do => {
1700 current_cmd.args.push("do".to_string());
1701 }
1702 Token::Done => {
1703 current_cmd.args.push("done".to_string());
1704 }
1705 Token::Pipe => {
1706 if !current_cmd.args.is_empty() || current_cmd.compound.is_some() {
1707 commands.push(current_cmd.clone());
1708 current_cmd = ShellCommand::default();
1709 }
1710 }
1711 // Basic redirections (backward compatible)
1712 Token::RedirIn => {
1713 i += 1;
1714 if i < tokens.len()
1715 && let Token::Word(ref file) = tokens[i]
1716 {
1717 current_cmd
1718 .redirections
1719 .push(Redirection::Input(file.clone()));
1720 }
1721 }
1722 Token::RedirOut => {
1723 i += 1;
1724 if i < tokens.len()
1725 && let Token::Word(ref file) = tokens[i]
1726 {
1727 current_cmd
1728 .redirections
1729 .push(Redirection::Output(file.clone()));
1730 }
1731 }
1732 Token::RedirOutClobber => {
1733 i += 1;
1734 if i >= tokens.len() {
1735 return Err("expected filename after >|".to_string());
1736 }
1737 if let Token::Word(ref file) = tokens[i] {
1738 current_cmd
1739 .redirections
1740 .push(Redirection::OutputClobber(file.clone()));
1741 } else {
1742 return Err("expected filename after >|".to_string());
1743 }
1744 }
1745 Token::RedirAppend => {
1746 i += 1;
1747 if i < tokens.len()
1748 && let Token::Word(ref file) = tokens[i]
1749 {
1750 current_cmd
1751 .redirections
1752 .push(Redirection::Append(file.clone()));
1753 }
1754 }
1755 Token::RedirHereDoc(delimiter, quoted) => {
1756 // Store delimiter and quoted flag - content will be read by executor
1757 current_cmd
1758 .redirections
1759 .push(Redirection::HereDoc(delimiter.clone(), quoted.to_string()));
1760 }
1761 Token::RedirHereString(content) => {
1762 current_cmd
1763 .redirections
1764 .push(Redirection::HereString(content.clone()));
1765 }
1766 // File descriptor redirections
1767 Token::RedirectFdIn(fd, file) => {
1768 current_cmd
1769 .redirections
1770 .push(Redirection::FdInput(*fd, file.clone()));
1771 }
1772 Token::RedirectFdOut(fd, file) => {
1773 current_cmd
1774 .redirections
1775 .push(Redirection::FdOutput(*fd, file.clone()));
1776 }
1777 Token::RedirectFdOutClobber(fd, file) => {
1778 current_cmd
1779 .redirections
1780 .push(Redirection::FdOutputClobber(*fd, file.clone()));
1781 }
1782 Token::RedirectFdAppend(fd, file) => {
1783 current_cmd
1784 .redirections
1785 .push(Redirection::FdAppend(*fd, file.clone()));
1786 }
1787 Token::RedirectFdDup(from_fd, to_fd) => {
1788 current_cmd
1789 .redirections
1790 .push(Redirection::FdDuplicate(*from_fd, *to_fd));
1791 }
1792 Token::RedirectFdClose(fd) => {
1793 current_cmd.redirections.push(Redirection::FdClose(*fd));
1794 }
1795 Token::RedirectFdInOut(fd, file) => {
1796 current_cmd
1797 .redirections
1798 .push(Redirection::FdInputOutput(*fd, file.clone()));
1799 }
1800 Token::RightParen => {
1801 // Check if this looks like a function call pattern: Word LeftParen ... RightParen
1802 // If so, treat it as a function call even if the function doesn't exist
1803 if !current_cmd.args.is_empty()
1804 && i > 0
1805 && let Token::LeftParen = tokens[i - 1]
1806 {
1807 // This looks like a function call pattern, treat as function call
1808 // For now, we'll handle this in the executor by checking if it's a function
1809 // If not a function, the executor will handle the error gracefully
1810 break;
1811 }
1812 return Err("Unexpected ) in pipeline".to_string());
1813 }
1814 Token::Newline => {
1815 // Ignore newlines in pipelines if they follow a pipe or if we are at the start of a stage
1816 if current_cmd.args.is_empty() && current_cmd.compound.is_none() {
1817 // This newline is between commands or at the start, skip it
1818 } else {
1819 break;
1820 }
1821 }
1822 Token::And | Token::Or | Token::Semicolon => {
1823 // These tokens end the current pipeline
1824 // They will be handled by parse_commands_sequentially
1825 break;
1826 }
1827 _ => {
1828 return Err(format!("Unexpected token in pipeline: {:?}", token));
1829 }
1830 }
1831 i += 1;
1832 }
1833
1834 if !current_cmd.args.is_empty() || current_cmd.compound.is_some() {
1835 commands.push(current_cmd);
1836 }
1837
1838 if commands.is_empty() {
1839 return Err("No commands found".to_string());
1840 }
1841
1842 Ok(Ast::Pipeline(commands))
1843}
1844
1845#[cfg(test)]
1846mod tests;