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;