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 => {
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 ast = parse_slice(command_tokens)?;
537    Ok((ast, i))
538}
539
540/// Helper function to parse commands with && and || operators.
541/// This builds a left-associative chain of operators.
542/// Returns the parsed AST and the number of tokens consumed.
543fn parse_next_command(tokens: &[Token]) -> Result<(Ast, usize), String> {
544    // Parse the first command
545    let (mut ast, mut i) = parse_single_command(tokens)?;
546
547    // Build left-associative chain of && and || operators iteratively
548    loop {
549        // Check if there's an && or || operator after this command
550        if i >= tokens.len() || (tokens[i] != Token::And && tokens[i] != Token::Or) {
551            break;
552        }
553
554        let operator = tokens[i].clone();
555        i += 1; // Skip the operator
556
557        // Skip any newlines after the operator
558        while i < tokens.len() && tokens[i] == Token::Newline {
559            i += 1;
560        }
561
562        if i >= tokens.len() {
563            return Err("Expected command after operator".to_string());
564        }
565
566        // Parse the next single command (without chaining)
567        let (right_ast, consumed) = parse_single_command(&tokens[i..])?;
568        i += consumed;
569
570        // Build left-associative structure
571        ast = match operator {
572            Token::And => Ast::And {
573                left: Box::new(ast),
574                right: Box::new(right_ast),
575            },
576            Token::Or => Ast::Or {
577                left: Box::new(ast),
578                right: Box::new(right_ast),
579            },
580            _ => unreachable!(),
581        };
582    }
583
584    Ok((ast, i))
585}
586
587/// Parses a slice of tokens into a top-level AST representing one or more sequential shell commands.
588///
589/// This function consumes the provided token sequence and produces an `Ast` that represents either a
590/// single command/pipeline/compound construct or a `Sequence` of commands joined by semicolons/newlines
591/// and conditional operators. It recognizes subshells, command groups, pipelines, redirections,
592/// negation (`!`), function definitions, and control-flow blocks, and composes appropriate AST nodes.
593///
594/// # Errors
595///
596/// Returns an `Err(String)` when the tokens contain a syntactic problem that prevents building a valid AST,
597/// for example unmatched braces/parentheses, an empty subshell or command group, or when no commands are present.
598///
599/// # Examples
600///
601/// ```
602/// // Note: parse_commands_sequentially is a private function
603/// // This example is for documentation only
604/// ```
605fn parse_commands_sequentially(tokens: &[Token]) -> Result<Ast, String> {
606    let mut i = 0;
607    let mut commands = Vec::new();
608
609    while i < tokens.len() {
610        // Skip whitespace and comments
611        while i < tokens.len() {
612            match &tokens[i] {
613                Token::Newline => {
614                    i += 1;
615                }
616                Token::Word(word) if word.starts_with('#') => {
617                    // Skip comment line
618                    while i < tokens.len() && tokens[i] != Token::Newline {
619                        i += 1;
620                    }
621                    if i < tokens.len() {
622                        i += 1; // Skip the newline
623                    }
624                }
625                _ => break,
626            }
627        }
628
629        if i >= tokens.len() {
630            break;
631        }
632
633        // Find the end of this command
634        let start = i;
635
636        // Check for subshell: LeftParen at start of command
637        // Must check BEFORE function definition to avoid ambiguity
638        if tokens[i] == Token::LeftParen {
639            // This is a subshell - find the matching RightParen
640            let mut paren_depth = 1;
641            let mut j = i + 1;
642
643            while j < tokens.len() && paren_depth > 0 {
644                match tokens[j] {
645                    Token::LeftParen => paren_depth += 1,
646                    Token::RightParen => paren_depth -= 1,
647                    _ => {}
648                }
649                j += 1;
650            }
651
652            if paren_depth != 0 {
653                return Err("Unmatched parenthesis in subshell".to_string());
654            }
655
656            // Extract subshell body (tokens between parens)
657            let subshell_tokens = &tokens[i + 1..j - 1];
658
659            // Parse the subshell body recursively
660            // Empty subshells are not allowed
661            let body_ast = if subshell_tokens.is_empty() {
662                return Err("Empty subshell".to_string());
663            } else {
664                parse_commands_sequentially(subshell_tokens)?
665            };
666
667            let mut subshell_ast = Ast::Subshell {
668                body: Box::new(body_ast),
669            };
670
671            i = j; // Move past the closing paren
672
673            // Check for redirections after subshell
674            let mut redirections = Vec::new();
675            while i < tokens.len() {
676                match &tokens[i] {
677                    Token::RedirOut => {
678                        i += 1;
679                        if i < tokens.len()
680                            && let Token::Word(file) = &tokens[i]
681                        {
682                            redirections.push(Redirection::Output(file.clone()));
683                            i += 1;
684                        }
685                    }
686                    Token::RedirOutClobber => {
687                        i += 1;
688                        if i >= tokens.len() {
689                            return Err("expected filename after >|".to_string());
690                        }
691                        if let Token::Word(file) = &tokens[i] {
692                            redirections.push(Redirection::OutputClobber(file.clone()));
693                            i += 1;
694                        } else {
695                            return Err("expected filename after >|".to_string());
696                        }
697                    }
698                    Token::RedirIn => {
699                        i += 1;
700                        if i < tokens.len()
701                            && let Token::Word(file) = &tokens[i]
702                        {
703                            redirections.push(Redirection::Input(file.clone()));
704                            i += 1;
705                        }
706                    }
707                    Token::RedirAppend => {
708                        i += 1;
709                        if i < tokens.len()
710                            && let Token::Word(file) = &tokens[i]
711                        {
712                            redirections.push(Redirection::Append(file.clone()));
713                            i += 1;
714                        }
715                    }
716                    Token::RedirectFdOut(fd, file) => {
717                        redirections.push(Redirection::FdOutput(*fd, file.clone()));
718                        i += 1;
719                    }
720                    Token::RedirectFdOutClobber(fd, file) => {
721                        redirections.push(Redirection::FdOutputClobber(*fd, file.clone()));
722                        i += 1;
723                    }
724                    Token::RedirectFdIn(fd, file) => {
725                        redirections.push(Redirection::FdInput(*fd, file.clone()));
726                        i += 1;
727                    }
728                    Token::RedirectFdAppend(fd, file) => {
729                        redirections.push(Redirection::FdAppend(*fd, file.clone()));
730                        i += 1;
731                    }
732                    Token::RedirectFdDup(from_fd, to_fd) => {
733                        redirections.push(Redirection::FdDuplicate(*from_fd, *to_fd));
734                        i += 1;
735                    }
736                    Token::RedirectFdClose(fd) => {
737                        redirections.push(Redirection::FdClose(*fd));
738                        i += 1;
739                    }
740                    Token::RedirectFdInOut(fd, file) => {
741                        redirections.push(Redirection::FdInputOutput(*fd, file.clone()));
742                        i += 1;
743                    }
744                    Token::RedirHereDoc(delimiter, quoted) => {
745                        redirections
746                            .push(Redirection::HereDoc(delimiter.clone(), quoted.to_string()));
747                        i += 1;
748                    }
749                    Token::RedirHereString(content) => {
750                        redirections.push(Redirection::HereString(content.clone()));
751                        i += 1;
752                    }
753                    _ => break,
754                }
755            }
756
757            // Check if this subshell is part of a pipeline
758            if i < tokens.len() && tokens[i] == Token::Pipe {
759                // Find end of pipeline
760                let mut end = i;
761                let mut brace_depth = 0;
762                let mut paren_depth = 0;
763                let mut last_was_pipe = true; // Started with a pipe
764                while end < tokens.len() {
765                    match &tokens[end] {
766                        Token::Pipe => last_was_pipe = true,
767                        Token::LeftBrace => {
768                            brace_depth += 1;
769                            last_was_pipe = false;
770                        }
771                        Token::RightBrace => {
772                            if brace_depth > 0 {
773                                brace_depth -= 1;
774                            } else {
775                                break;
776                            }
777                            last_was_pipe = false;
778                        }
779                        Token::LeftParen => {
780                            paren_depth += 1;
781                            last_was_pipe = false;
782                        }
783                        Token::RightParen => {
784                            if paren_depth > 0 {
785                                paren_depth -= 1;
786                            } else {
787                                break;
788                            }
789                            last_was_pipe = false;
790                        }
791                        Token::Newline | Token::Semicolon => {
792                            if brace_depth == 0 && paren_depth == 0 && !last_was_pipe {
793                                break;
794                            }
795                        }
796                        Token::Word(_) => last_was_pipe = false,
797                        _ => {}
798                    }
799                    end += 1;
800                }
801
802                let pipeline_ast = parse_pipeline(&tokens[start..end])?;
803                commands.push(pipeline_ast);
804                i = end;
805                continue;
806            }
807
808            // If not part of a pipeline, apply redirections to the subshell itself
809            if !redirections.is_empty() {
810                subshell_ast = Ast::Pipeline(vec![ShellCommand {
811                    args: Vec::new(),
812                    redirections,
813                    compound: Some(Box::new(subshell_ast)),
814                }]);
815            }
816
817            // Handle operators after subshell (&&, ||, ;, newline)
818            if i < tokens.len() && (tokens[i] == Token::And || tokens[i] == Token::Or) {
819                let operator = tokens[i].clone();
820                i += 1; // Skip the operator
821
822                // Skip any newlines after the operator
823                while i < tokens.len() && tokens[i] == Token::Newline {
824                    i += 1;
825                }
826
827                // Parse only the next command (not the entire remaining sequence)
828                let (right_ast, consumed) = parse_next_command(&tokens[i..])?;
829                i += consumed;
830
831                // Create And or Or node
832                let combined_ast = match operator {
833                    Token::And => Ast::And {
834                        left: Box::new(subshell_ast),
835                        right: Box::new(right_ast),
836                    },
837                    Token::Or => Ast::Or {
838                        left: Box::new(subshell_ast),
839                        right: Box::new(right_ast),
840                    },
841                    _ => unreachable!(),
842                };
843
844                commands.push(combined_ast);
845
846                // Skip semicolon or newline after the combined command
847                if i < tokens.len()
848                    && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon)
849                {
850                    i += 1;
851                }
852                continue;
853            } else {
854                commands.push(subshell_ast);
855            }
856
857            // Skip semicolon or newline after subshell
858            if i < tokens.len() && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon) {
859                i += 1;
860            }
861            continue;
862        }
863
864        // Check for command group: LeftBrace at start of command
865        if tokens[i] == Token::LeftBrace {
866            // This is a command group - find the matching RightBrace
867            let mut brace_depth = 1;
868            let mut j = i + 1;
869
870            while j < tokens.len() && brace_depth > 0 {
871                match tokens[j] {
872                    Token::LeftBrace => brace_depth += 1,
873                    Token::RightBrace => brace_depth -= 1,
874                    _ => {}
875                }
876                j += 1;
877            }
878
879            if brace_depth != 0 {
880                return Err("Unmatched brace in command group".to_string());
881            }
882
883            // Extract group body (tokens between braces)
884            let group_tokens = &tokens[i + 1..j - 1];
885
886            // Parse the group body recursively
887            // Empty groups are not allowed
888            let body_ast = if group_tokens.is_empty() {
889                return Err("Empty command group".to_string());
890            } else {
891                parse_commands_sequentially(group_tokens)?
892            };
893
894            let mut group_ast = Ast::CommandGroup {
895                body: Box::new(body_ast),
896            };
897
898            i = j; // Move past the closing brace
899
900            // Check for redirections after command group
901            let mut redirections = Vec::new();
902            while i < tokens.len() {
903                match &tokens[i] {
904                    Token::RedirOut => {
905                        i += 1;
906                        if i < tokens.len()
907                            && let Token::Word(file) = &tokens[i]
908                        {
909                            redirections.push(Redirection::Output(file.clone()));
910                            i += 1;
911                        }
912                    }
913                    Token::RedirOutClobber => {
914                        i += 1;
915                        if i >= tokens.len() {
916                            return Err("expected filename after >|".to_string());
917                        }
918                        if let Token::Word(file) = &tokens[i] {
919                            redirections.push(Redirection::OutputClobber(file.clone()));
920                            i += 1;
921                        } else {
922                            return Err("expected filename after >|".to_string());
923                        }
924                    }
925                    Token::RedirIn => {
926                        i += 1;
927                        if i < tokens.len()
928                            && let Token::Word(file) = &tokens[i]
929                        {
930                            redirections.push(Redirection::Input(file.clone()));
931                            i += 1;
932                        }
933                    }
934                    Token::RedirAppend => {
935                        i += 1;
936                        if i < tokens.len()
937                            && let Token::Word(file) = &tokens[i]
938                        {
939                            redirections.push(Redirection::Append(file.clone()));
940                            i += 1;
941                        }
942                    }
943                    Token::RedirectFdOut(fd, file) => {
944                        redirections.push(Redirection::FdOutput(*fd, file.clone()));
945                        i += 1;
946                    }
947                    Token::RedirectFdIn(fd, file) => {
948                        redirections.push(Redirection::FdInput(*fd, file.clone()));
949                        i += 1;
950                    }
951                    Token::RedirectFdAppend(fd, file) => {
952                        redirections.push(Redirection::FdAppend(*fd, file.clone()));
953                        i += 1;
954                    }
955                    Token::RedirectFdDup(from_fd, to_fd) => {
956                        redirections.push(Redirection::FdDuplicate(*from_fd, *to_fd));
957                        i += 1;
958                    }
959                    Token::RedirectFdClose(fd) => {
960                        redirections.push(Redirection::FdClose(*fd));
961                        i += 1;
962                    }
963                    Token::RedirectFdInOut(fd, file) => {
964                        redirections.push(Redirection::FdInputOutput(*fd, file.clone()));
965                        i += 1;
966                    }
967                    Token::RedirHereDoc(delimiter, quoted) => {
968                        redirections
969                            .push(Redirection::HereDoc(delimiter.clone(), quoted.to_string()));
970                        i += 1;
971                    }
972                    Token::RedirHereString(content) => {
973                        redirections.push(Redirection::HereString(content.clone()));
974                        i += 1;
975                    }
976                    _ => break,
977                }
978            }
979
980            // Check if this group is part of a pipeline
981            if i < tokens.len() && tokens[i] == Token::Pipe {
982                // Find end of pipeline
983                let mut end = i;
984                let mut brace_depth = 0;
985                let mut paren_depth = 0;
986                let mut last_was_pipe = true; // Started with a pipe
987                while end < tokens.len() {
988                    match &tokens[end] {
989                        Token::Pipe => last_was_pipe = true,
990                        Token::LeftBrace => {
991                            brace_depth += 1;
992                            last_was_pipe = false;
993                        }
994                        Token::RightBrace => {
995                            if brace_depth > 0 {
996                                brace_depth -= 1;
997                            } else {
998                                break;
999                            }
1000                            last_was_pipe = false;
1001                        }
1002                        Token::LeftParen => {
1003                            paren_depth += 1;
1004                            last_was_pipe = false;
1005                        }
1006                        Token::RightParen => {
1007                            if paren_depth > 0 {
1008                                paren_depth -= 1;
1009                            } else {
1010                                break;
1011                            }
1012                            last_was_pipe = false;
1013                        }
1014                        Token::Newline | Token::Semicolon => {
1015                            if brace_depth == 0 && paren_depth == 0 && !last_was_pipe {
1016                                break;
1017                            }
1018                        }
1019                        Token::Word(_) => last_was_pipe = false,
1020                        _ => {}
1021                    }
1022                    end += 1;
1023                }
1024
1025                let pipeline_ast = parse_pipeline(&tokens[start..end])?;
1026                commands.push(pipeline_ast);
1027                i = end;
1028                continue;
1029            }
1030
1031            // If not part of a pipeline, apply redirections to the group itself
1032            if !redirections.is_empty() {
1033                group_ast = Ast::Pipeline(vec![ShellCommand {
1034                    args: Vec::new(),
1035                    redirections,
1036                    compound: Some(Box::new(group_ast)),
1037                }]);
1038            }
1039
1040            // Handle operators after group (&&, ||, ;, newline)
1041            if i < tokens.len() && (tokens[i] == Token::And || tokens[i] == Token::Or) {
1042                let operator = tokens[i].clone();
1043                i += 1; // Skip the operator
1044
1045                // Skip any newlines after the operator
1046                while i < tokens.len() && tokens[i] == Token::Newline {
1047                    i += 1;
1048                }
1049
1050                // Parse only the next command (not the entire remaining sequence)
1051                let (right_ast, consumed) = parse_next_command(&tokens[i..])?;
1052                i += consumed;
1053
1054                // Create And or Or node
1055                let combined_ast = match operator {
1056                    Token::And => Ast::And {
1057                        left: Box::new(group_ast),
1058                        right: Box::new(right_ast),
1059                    },
1060                    Token::Or => Ast::Or {
1061                        left: Box::new(group_ast),
1062                        right: Box::new(right_ast),
1063                    },
1064                    _ => unreachable!(),
1065                };
1066
1067                commands.push(combined_ast);
1068
1069                // Skip semicolon or newline after the combined command
1070                if i < tokens.len()
1071                    && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon)
1072                {
1073                    i += 1;
1074                }
1075                continue;
1076            } else {
1077                commands.push(group_ast);
1078            }
1079
1080            // Skip semicolon or newline after group
1081            if i < tokens.len() && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon) {
1082                i += 1;
1083            }
1084            continue;
1085        }
1086
1087        // Special handling for compound commands
1088        if tokens[i] == Token::If {
1089            // For if statements, find the matching fi
1090            let mut depth = 0;
1091            while i < tokens.len() {
1092                match tokens[i] {
1093                    Token::If => depth += 1,
1094                    Token::Fi => {
1095                        depth -= 1;
1096                        if depth == 0 {
1097                            i += 1; // Include the fi
1098                            break;
1099                        }
1100                    }
1101                    _ => {}
1102                }
1103                i += 1;
1104            }
1105
1106            // If we didn't find a matching fi, include all remaining tokens
1107            // This handles the case where the if statement is incomplete
1108        } else if tokens[i] == Token::For {
1109            // For for loops, find the matching done
1110            let mut depth = 1; // Start at 1 because we're already inside the for
1111            i += 1; // Move past the 'for' token
1112            while i < tokens.len() {
1113                match tokens[i] {
1114                    Token::For | Token::While | Token::Until => depth += 1,
1115                    Token::Done => {
1116                        depth -= 1;
1117                        if depth == 0 {
1118                            i += 1; // Include the done
1119                            break;
1120                        }
1121                    }
1122                    _ => {}
1123                }
1124                i += 1;
1125            }
1126        } else if tokens[i] == Token::While {
1127            // For while loops, find the matching done
1128            let mut depth = 1; // Start at 1 because we're already inside the while
1129            i += 1; // Move past the 'while' token
1130            while i < tokens.len() {
1131                match tokens[i] {
1132                    Token::While | Token::For | Token::Until => depth += 1,
1133                    Token::Done => {
1134                        depth -= 1;
1135                        if depth == 0 {
1136                            i += 1; // Include the done
1137                            break;
1138                        }
1139                    }
1140                    _ => {}
1141                }
1142                i += 1;
1143            }
1144        } else if tokens[i] == Token::Until {
1145            // For until loops, find the matching done
1146            let mut depth = 1; // Start at 1 because we're already inside the until
1147            i += 1; // Move past the 'until' token
1148            while i < tokens.len() {
1149                match tokens[i] {
1150                    Token::Until | Token::For | Token::While => depth += 1,
1151                    Token::Done => {
1152                        depth -= 1;
1153                        if depth == 0 {
1154                            i += 1; // Include the done
1155                            break;
1156                        }
1157                    }
1158                    _ => {}
1159                }
1160                i += 1;
1161            }
1162        } else if tokens[i] == Token::Case {
1163            // For case statements, find the matching esac
1164            while i < tokens.len() {
1165                if tokens[i] == Token::Esac {
1166                    i += 1; // Include the esac
1167                    break;
1168                }
1169                i += 1;
1170            }
1171        } else if i + 3 < tokens.len()
1172            && matches!(tokens[i], Token::Word(_))
1173            && tokens[i + 1] == Token::LeftParen
1174            && tokens[i + 2] == Token::RightParen
1175            && tokens[i + 3] == Token::LeftBrace
1176        {
1177            // This is a function definition - find the matching closing brace
1178            let mut brace_depth = 1;
1179            i += 4; // Skip to after opening brace
1180            while i < tokens.len() && brace_depth > 0 {
1181                match tokens[i] {
1182                    Token::LeftBrace => brace_depth += 1,
1183                    Token::RightBrace => brace_depth -= 1,
1184                    _ => {}
1185                }
1186                i += 1;
1187            }
1188        } else {
1189            // Sanity check: we shouldn't be starting a command with an operator
1190            if matches!(tokens[i], Token::And | Token::Or | Token::Semicolon) {
1191                return Err(format!(
1192                    "Unexpected operator at command start: {:?}",
1193                    tokens[i]
1194                ));
1195            }
1196
1197            // For simple commands, stop at newline, semicolon, &&, or ||
1198            // But check if the next token after newline is a control flow keyword
1199            let mut brace_depth = 0;
1200            let mut paren_depth = 0;
1201            let mut last_was_pipe = false;
1202            while i < tokens.len() {
1203                match &tokens[i] {
1204                    Token::LeftBrace => {
1205                        brace_depth += 1;
1206                        last_was_pipe = false;
1207                    }
1208                    Token::RightBrace => {
1209                        if brace_depth > 0 {
1210                            brace_depth -= 1;
1211                        } else {
1212                            break;
1213                        }
1214                        last_was_pipe = false;
1215                    }
1216                    Token::LeftParen => {
1217                        paren_depth += 1;
1218                        last_was_pipe = false;
1219                    }
1220                    Token::RightParen => {
1221                        if paren_depth > 0 {
1222                            paren_depth -= 1;
1223                        } else {
1224                            break;
1225                        }
1226                        last_was_pipe = false;
1227                    }
1228                    Token::Pipe => last_was_pipe = true,
1229                    Token::Newline | Token::Semicolon | Token::And | Token::Or => {
1230                        if brace_depth == 0 && paren_depth == 0 && !last_was_pipe {
1231                            break;
1232                        }
1233                    }
1234                    Token::Word(_) => last_was_pipe = false,
1235                    _ => {}
1236                }
1237                i += 1;
1238            }
1239        }
1240
1241        let command_tokens = &tokens[start..i];
1242        if !command_tokens.is_empty() {
1243            // Don't try to parse orphaned else/elif/fi tokens
1244            if command_tokens.len() == 1 {
1245                match command_tokens[0] {
1246                    Token::Else | Token::Elif | Token::Fi => {
1247                        // Skip orphaned control flow tokens
1248                        if i < tokens.len()
1249                            && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon)
1250                        {
1251                            i += 1;
1252                        }
1253                        continue;
1254                    }
1255                    _ => {}
1256                }
1257            }
1258
1259            // Use parse_next_command to handle operators
1260            let (ast, consumed) = parse_next_command(&tokens[start..])?;
1261            i = start + consumed;
1262
1263            commands.push(ast);
1264        }
1265
1266        if i < tokens.len() && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon) {
1267            i += 1;
1268        }
1269    }
1270
1271    if commands.is_empty() {
1272        return Err("No commands found".to_string());
1273    }
1274
1275    if commands.len() == 1 {
1276        Ok(commands.into_iter().next().unwrap())
1277    } else {
1278        Ok(Ast::Sequence(commands))
1279    }
1280}
1281
1282/// Parses a sequence of tokens into an `Ast::Pipeline` representing one or more pipeline stages.
1283///
1284/// The resulting pipeline contains one `ShellCommand` per stage with collected `args`,
1285/// ordered `redirections`, and an optional `compound` (subshell or command group). Returns an
1286/// error if the tokens contain unmatched braces/parentheses, an unexpected token, or no commands.
1287///
1288/// # Examples
1289///
1290/// ```
1291/// // Note: parse_pipeline is a private function
1292/// // This example is for documentation only
1293/// ```
1294fn parse_pipeline(tokens: &[Token]) -> Result<Ast, String> {
1295    let mut commands = Vec::new();
1296    let mut current_cmd = ShellCommand::default();
1297
1298    let mut i = 0;
1299    while i < tokens.len() {
1300        let token = &tokens[i];
1301        match token {
1302            Token::LeftBrace => {
1303                // Start of command group in pipeline
1304                // Find matching RightBrace
1305                let mut brace_depth = 1;
1306                let mut j = i + 1;
1307
1308                while j < tokens.len() && brace_depth > 0 {
1309                    match tokens[j] {
1310                        Token::LeftBrace => brace_depth += 1,
1311                        Token::RightBrace => brace_depth -= 1,
1312                        _ => {}
1313                    }
1314                    j += 1;
1315                }
1316
1317                if brace_depth != 0 {
1318                    return Err("Unmatched brace in pipeline".to_string());
1319                }
1320
1321                // Parse group body
1322                let group_tokens = &tokens[i + 1..j - 1];
1323
1324                // Empty groups are valid and equivalent to 'true'
1325                let body_ast = if group_tokens.is_empty() {
1326                    create_empty_body_ast()
1327                } else {
1328                    parse_commands_sequentially(group_tokens)?
1329                };
1330
1331                // Create ShellCommand with compound command group
1332                current_cmd.compound = Some(Box::new(Ast::CommandGroup {
1333                    body: Box::new(body_ast),
1334                }));
1335
1336                i = j; // Move past closing brace
1337
1338                // Check for redirections after command group
1339                while i < tokens.len() {
1340                    match &tokens[i] {
1341                        Token::RedirOut => {
1342                            i += 1;
1343                            if i < tokens.len()
1344                                && let Token::Word(file) = &tokens[i]
1345                            {
1346                                current_cmd
1347                                    .redirections
1348                                    .push(Redirection::Output(file.clone()));
1349                                i += 1;
1350                            }
1351                        }
1352                        Token::RedirOutClobber => {
1353                            i += 1;
1354                            if i >= tokens.len() {
1355                                return Err("expected filename after >|".to_string());
1356                            }
1357                            if let Token::Word(file) = &tokens[i] {
1358                                current_cmd
1359                                    .redirections
1360                                    .push(Redirection::OutputClobber(file.clone()));
1361                                i += 1;
1362                            } else {
1363                                return Err("expected filename after >|".to_string());
1364                            }
1365                        }
1366                        Token::RedirIn => {
1367                            i += 1;
1368                            if i < tokens.len()
1369                                && let Token::Word(file) = &tokens[i]
1370                            {
1371                                current_cmd
1372                                    .redirections
1373                                    .push(Redirection::Input(file.clone()));
1374                                i += 1;
1375                            }
1376                        }
1377                        Token::RedirAppend => {
1378                            i += 1;
1379                            if i < tokens.len()
1380                                && let Token::Word(file) = &tokens[i]
1381                            {
1382                                current_cmd
1383                                    .redirections
1384                                    .push(Redirection::Append(file.clone()));
1385                                i += 1;
1386                            }
1387                        }
1388                        Token::RedirectFdOut(fd, file) => {
1389                            current_cmd
1390                                .redirections
1391                                .push(Redirection::FdOutput(*fd, file.clone()));
1392                            i += 1;
1393                        }
1394                        Token::RedirectFdOutClobber(fd, file) => {
1395                            current_cmd
1396                                .redirections
1397                                .push(Redirection::FdOutputClobber(*fd, file.clone()));
1398                            i += 1;
1399                        }
1400                        Token::RedirectFdIn(fd, file) => {
1401                            current_cmd
1402                                .redirections
1403                                .push(Redirection::FdInput(*fd, file.clone()));
1404                            i += 1;
1405                        }
1406                        Token::RedirectFdAppend(fd, file) => {
1407                            current_cmd
1408                                .redirections
1409                                .push(Redirection::FdAppend(*fd, file.clone()));
1410                            i += 1;
1411                        }
1412                        Token::RedirectFdDup(from_fd, to_fd) => {
1413                            current_cmd
1414                                .redirections
1415                                .push(Redirection::FdDuplicate(*from_fd, *to_fd));
1416                            i += 1;
1417                        }
1418                        Token::RedirectFdClose(fd) => {
1419                            current_cmd.redirections.push(Redirection::FdClose(*fd));
1420                            i += 1;
1421                        }
1422                        Token::RedirectFdInOut(fd, file) => {
1423                            current_cmd
1424                                .redirections
1425                                .push(Redirection::FdInputOutput(*fd, file.clone()));
1426                            i += 1;
1427                        }
1428                        Token::RedirHereDoc(delimiter, quoted) => {
1429                            current_cmd
1430                                .redirections
1431                                .push(Redirection::HereDoc(delimiter.clone(), quoted.to_string()));
1432                            i += 1;
1433                        }
1434                        Token::RedirHereString(content) => {
1435                            current_cmd
1436                                .redirections
1437                                .push(Redirection::HereString(content.clone()));
1438                            i += 1;
1439                        }
1440                        Token::Pipe => {
1441                            // End of this pipeline stage
1442                            break;
1443                        }
1444                        _ => break,
1445                    }
1446                }
1447
1448                // Stage will be pushed at next | or end of loop
1449                continue;
1450            }
1451            Token::LeftParen => {
1452                // Start of subshell in pipeline
1453                // Find matching RightParen
1454                let mut paren_depth = 1;
1455                let mut j = i + 1;
1456
1457                while j < tokens.len() && paren_depth > 0 {
1458                    match tokens[j] {
1459                        Token::LeftParen => paren_depth += 1,
1460                        Token::RightParen => paren_depth -= 1,
1461                        _ => {}
1462                    }
1463                    j += 1;
1464                }
1465
1466                if paren_depth != 0 {
1467                    return Err("Unmatched parenthesis in pipeline".to_string());
1468                }
1469
1470                // Parse subshell body
1471                let subshell_tokens = &tokens[i + 1..j - 1];
1472
1473                // Empty subshells are valid and equivalent to 'true'
1474                let body_ast = if subshell_tokens.is_empty() {
1475                    create_empty_body_ast()
1476                } else {
1477                    parse_commands_sequentially(subshell_tokens)?
1478                };
1479
1480                // Create ShellCommand with compound subshell
1481                // Create ShellCommand with compound subshell
1482                current_cmd.compound = Some(Box::new(Ast::Subshell {
1483                    body: Box::new(body_ast),
1484                }));
1485
1486                i = j; // Move past closing paren
1487
1488                // Check for redirections after subshell
1489                while i < tokens.len() {
1490                    match &tokens[i] {
1491                        Token::RedirOut => {
1492                            i += 1;
1493                            if i < tokens.len()
1494                                && let Token::Word(file) = &tokens[i]
1495                            {
1496                                current_cmd
1497                                    .redirections
1498                                    .push(Redirection::Output(file.clone()));
1499                                i += 1;
1500                            }
1501                        }
1502                        Token::RedirOutClobber => {
1503                            i += 1;
1504                            if i >= tokens.len() {
1505                                return Err("expected filename after >|".to_string());
1506                            }
1507                            if let Token::Word(file) = &tokens[i] {
1508                                current_cmd
1509                                    .redirections
1510                                    .push(Redirection::OutputClobber(file.clone()));
1511                                i += 1;
1512                            } else {
1513                                return Err("expected filename after >|".to_string());
1514                            }
1515                        }
1516                        Token::RedirIn => {
1517                            i += 1;
1518                            if i < tokens.len()
1519                                && let Token::Word(file) = &tokens[i]
1520                            {
1521                                current_cmd
1522                                    .redirections
1523                                    .push(Redirection::Input(file.clone()));
1524                                i += 1;
1525                            }
1526                        }
1527                        Token::RedirAppend => {
1528                            i += 1;
1529                            if i < tokens.len()
1530                                && let Token::Word(file) = &tokens[i]
1531                            {
1532                                current_cmd
1533                                    .redirections
1534                                    .push(Redirection::Append(file.clone()));
1535                                i += 1;
1536                            }
1537                        }
1538                        Token::RedirectFdOut(fd, file) => {
1539                            current_cmd
1540                                .redirections
1541                                .push(Redirection::FdOutput(*fd, file.clone()));
1542                            i += 1;
1543                        }
1544                        Token::RedirectFdOutClobber(fd, file) => {
1545                            current_cmd
1546                                .redirections
1547                                .push(Redirection::FdOutputClobber(*fd, file.clone()));
1548                            i += 1;
1549                        }
1550                        Token::RedirectFdIn(fd, file) => {
1551                            current_cmd
1552                                .redirections
1553                                .push(Redirection::FdInput(*fd, file.clone()));
1554                            i += 1;
1555                        }
1556                        Token::RedirectFdAppend(fd, file) => {
1557                            current_cmd
1558                                .redirections
1559                                .push(Redirection::FdAppend(*fd, file.clone()));
1560                            i += 1;
1561                        }
1562                        Token::RedirectFdDup(from_fd, to_fd) => {
1563                            current_cmd
1564                                .redirections
1565                                .push(Redirection::FdDuplicate(*from_fd, *to_fd));
1566                            i += 1;
1567                        }
1568                        Token::RedirectFdClose(fd) => {
1569                            current_cmd.redirections.push(Redirection::FdClose(*fd));
1570                            i += 1;
1571                        }
1572                        Token::RedirectFdInOut(fd, file) => {
1573                            current_cmd
1574                                .redirections
1575                                .push(Redirection::FdInputOutput(*fd, file.clone()));
1576                            i += 1;
1577                        }
1578                        Token::RedirHereDoc(delimiter, quoted) => {
1579                            current_cmd
1580                                .redirections
1581                                .push(Redirection::HereDoc(delimiter.clone(), quoted.to_string()));
1582                            i += 1;
1583                        }
1584                        Token::RedirHereString(content) => {
1585                            current_cmd
1586                                .redirections
1587                                .push(Redirection::HereString(content.clone()));
1588                            i += 1;
1589                        }
1590                        Token::Pipe => {
1591                            // End of this pipeline stage
1592                            break;
1593                        }
1594                        _ => break,
1595                    }
1596                }
1597
1598                // Stage will be pushed at next | or end of loop
1599                continue;
1600            }
1601            Token::Word(word) => {
1602                current_cmd.args.push(word.clone());
1603            }
1604            Token::Local => {
1605                current_cmd.args.push("local".to_string());
1606            }
1607            Token::Return => {
1608                current_cmd.args.push("return".to_string());
1609            }
1610            Token::Break => {
1611                current_cmd.args.push("break".to_string());
1612            }
1613            Token::Continue => {
1614                current_cmd.args.push("continue".to_string());
1615            }
1616            // Handle keywords as command arguments
1617            // When keywords appear in pipeline context (not at start of command),
1618            // they should be treated as regular word arguments
1619            Token::If => {
1620                current_cmd.args.push("if".to_string());
1621            }
1622            Token::Then => {
1623                current_cmd.args.push("then".to_string());
1624            }
1625            Token::Else => {
1626                current_cmd.args.push("else".to_string());
1627            }
1628            Token::Elif => {
1629                current_cmd.args.push("elif".to_string());
1630            }
1631            Token::Fi => {
1632                current_cmd.args.push("fi".to_string());
1633            }
1634            Token::Case => {
1635                current_cmd.args.push("case".to_string());
1636            }
1637            Token::In => {
1638                current_cmd.args.push("in".to_string());
1639            }
1640            Token::Esac => {
1641                current_cmd.args.push("esac".to_string());
1642            }
1643            Token::For => {
1644                current_cmd.args.push("for".to_string());
1645            }
1646            Token::While => {
1647                current_cmd.args.push("while".to_string());
1648            }
1649            Token::Until => {
1650                current_cmd.args.push("until".to_string());
1651            }
1652            Token::Do => {
1653                current_cmd.args.push("do".to_string());
1654            }
1655            Token::Done => {
1656                current_cmd.args.push("done".to_string());
1657            }
1658            Token::Pipe => {
1659                if !current_cmd.args.is_empty() || current_cmd.compound.is_some() {
1660                    commands.push(current_cmd.clone());
1661                    current_cmd = ShellCommand::default();
1662                }
1663            }
1664            // Basic redirections (backward compatible)
1665            Token::RedirIn => {
1666                i += 1;
1667                if i < tokens.len()
1668                    && let Token::Word(ref file) = tokens[i]
1669                {
1670                    current_cmd
1671                        .redirections
1672                        .push(Redirection::Input(file.clone()));
1673                }
1674            }
1675            Token::RedirOut => {
1676                i += 1;
1677                if i < tokens.len()
1678                    && let Token::Word(ref file) = tokens[i]
1679                {
1680                    current_cmd
1681                        .redirections
1682                        .push(Redirection::Output(file.clone()));
1683                }
1684            }
1685            Token::RedirOutClobber => {
1686                i += 1;
1687                if i >= tokens.len() {
1688                    return Err("expected filename after >|".to_string());
1689                }
1690                if let Token::Word(ref file) = tokens[i] {
1691                    current_cmd
1692                        .redirections
1693                        .push(Redirection::OutputClobber(file.clone()));
1694                } else {
1695                    return Err("expected filename after >|".to_string());
1696                }
1697            }
1698            Token::RedirAppend => {
1699                i += 1;
1700                if i < tokens.len()
1701                    && let Token::Word(ref file) = tokens[i]
1702                {
1703                    current_cmd
1704                        .redirections
1705                        .push(Redirection::Append(file.clone()));
1706                }
1707            }
1708            Token::RedirHereDoc(delimiter, quoted) => {
1709                // Store delimiter and quoted flag - content will be read by executor
1710                current_cmd
1711                    .redirections
1712                    .push(Redirection::HereDoc(delimiter.clone(), quoted.to_string()));
1713            }
1714            Token::RedirHereString(content) => {
1715                current_cmd
1716                    .redirections
1717                    .push(Redirection::HereString(content.clone()));
1718            }
1719            // File descriptor redirections
1720            Token::RedirectFdIn(fd, file) => {
1721                current_cmd
1722                    .redirections
1723                    .push(Redirection::FdInput(*fd, file.clone()));
1724            }
1725            Token::RedirectFdOut(fd, file) => {
1726                current_cmd
1727                    .redirections
1728                    .push(Redirection::FdOutput(*fd, file.clone()));
1729            }
1730            Token::RedirectFdOutClobber(fd, file) => {
1731                current_cmd
1732                    .redirections
1733                    .push(Redirection::FdOutputClobber(*fd, file.clone()));
1734            }
1735            Token::RedirectFdAppend(fd, file) => {
1736                current_cmd
1737                    .redirections
1738                    .push(Redirection::FdAppend(*fd, file.clone()));
1739            }
1740            Token::RedirectFdDup(from_fd, to_fd) => {
1741                current_cmd
1742                    .redirections
1743                    .push(Redirection::FdDuplicate(*from_fd, *to_fd));
1744            }
1745            Token::RedirectFdClose(fd) => {
1746                current_cmd.redirections.push(Redirection::FdClose(*fd));
1747            }
1748            Token::RedirectFdInOut(fd, file) => {
1749                current_cmd
1750                    .redirections
1751                    .push(Redirection::FdInputOutput(*fd, file.clone()));
1752            }
1753            Token::RightParen => {
1754                // Check if this looks like a function call pattern: Word LeftParen ... RightParen
1755                // If so, treat it as a function call even if the function doesn't exist
1756                if !current_cmd.args.is_empty()
1757                    && i > 0
1758                    && let Token::LeftParen = tokens[i - 1]
1759                {
1760                    // This looks like a function call pattern, treat as function call
1761                    // For now, we'll handle this in the executor by checking if it's a function
1762                    // If not a function, the executor will handle the error gracefully
1763                    break;
1764                }
1765                return Err("Unexpected ) in pipeline".to_string());
1766            }
1767            Token::Newline => {
1768                // Ignore newlines in pipelines if they follow a pipe or if we are at the start of a stage
1769                if current_cmd.args.is_empty() && current_cmd.compound.is_none() {
1770                    // This newline is between commands or at the start, skip it
1771                } else {
1772                    break;
1773                }
1774            }
1775            Token::And | Token::Or | Token::Semicolon => {
1776                // These tokens end the current pipeline
1777                // They will be handled by parse_commands_sequentially
1778                break;
1779            }
1780            _ => {
1781                return Err(format!("Unexpected token in pipeline: {:?}", token));
1782            }
1783        }
1784        i += 1;
1785    }
1786
1787    if !current_cmd.args.is_empty() || current_cmd.compound.is_some() {
1788        commands.push(current_cmd);
1789    }
1790
1791    if commands.is_empty() {
1792        return Err("No commands found".to_string());
1793    }
1794
1795    Ok(Ast::Pipeline(commands))
1796}
1797
1798#[cfg(test)]
1799mod tests;