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