rush_sh/
parser.rs

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