1use std::collections::HashSet;
2use std::env;
3
4use super::parameter_expansion::{expand_parameter, parse_parameter_expansion};
5use super::state::ShellState;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum Token {
9 Word(String),
10 Pipe,
11 RedirOut,
12 RedirIn,
13 RedirAppend,
14 RedirHereDoc(String, bool), RedirHereString(String), If,
17 Then,
18 Else,
19 Elif,
20 Fi,
21 Case,
22 In,
23 Esac,
24 DoubleSemicolon,
25 Semicolon,
26 RightParen,
27 LeftParen,
28 LeftBrace,
29 RightBrace,
30 Newline,
31 Local,
32 Return,
33 For,
34 Do,
35 Done,
36 While, And, Or, }
40
41fn is_keyword(word: &str) -> Option<Token> {
42 match word {
43 "if" => Some(Token::If),
44 "then" => Some(Token::Then),
45 "else" => Some(Token::Else),
46 "elif" => Some(Token::Elif),
47 "fi" => Some(Token::Fi),
48 "case" => Some(Token::Case),
49 "in" => Some(Token::In),
50 "esac" => Some(Token::Esac),
51 "local" => Some(Token::Local),
52 "return" => Some(Token::Return),
53 "for" => Some(Token::For),
54 "while" => Some(Token::While),
55 "do" => Some(Token::Do),
56 "done" => Some(Token::Done),
57 _ => None,
58 }
59}
60
61fn skip_whitespace(chars: &mut std::iter::Peekable<std::str::Chars>) {
63 while let Some(&ch) = chars.peek() {
64 if ch == ' ' || ch == '\t' {
65 chars.next();
66 } else {
67 break;
68 }
69 }
70}
71
72fn flush_current_token(current: &mut String, tokens: &mut Vec<Token>) {
74 if !current.is_empty() {
75 if let Some(keyword) = is_keyword(current) {
76 tokens.push(keyword);
77 } else {
78 tokens.push(Token::Word(current.clone()));
79 }
80 current.clear();
81 }
82}
83
84fn collect_until_closing_brace(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
87 let mut content = String::new();
88
89 while let Some(&ch) = chars.peek() {
90 if ch == '}' {
91 chars.next(); break;
93 } else {
94 content.push(ch);
95 chars.next();
96 }
97 }
98
99 content
100}
101
102fn collect_with_paren_depth(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
107 let mut content = String::new();
108 let mut paren_depth = 1; while let Some(&ch) = chars.peek() {
111 if ch == '(' {
112 paren_depth += 1;
113 content.push(ch);
114 chars.next();
115 } else if ch == ')' {
116 paren_depth -= 1;
117 if paren_depth == 0 {
118 chars.next(); break;
120 } else {
121 content.push(ch);
122 chars.next();
123 }
124 } else {
125 content.push(ch);
126 chars.next();
127 }
128 }
129
130 content
131}
132
133fn parse_variable_name(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
138 let mut var_name = String::new();
139
140 if let Some(&ch) = chars.peek() {
142 if ch == '?'
143 || ch == '$'
144 || ch == '0'
145 || ch == '#'
146 || ch == '@'
147 || ch == '*'
148 || ch == '!'
149 || ch.is_ascii_digit()
150 {
151 var_name.push(ch);
152 chars.next();
153 } else {
154 while let Some(&ch) = chars.peek() {
157 if ch.is_alphanumeric() || ch == '_' {
158 var_name.push(ch);
159 chars.next();
160 } else {
161 break;
162 }
163 }
164 }
165 }
166
167 var_name
168}
169
170fn expand_variables_in_command(command: &str, shell_state: &ShellState) -> String {
171 if command.contains("$(") || command.contains('`') {
173 return command.to_string();
174 }
175
176 let mut chars = command.chars().peekable();
177 let mut current = String::new();
178
179 while let Some(&ch) = chars.peek() {
180 if ch == '$' {
181 chars.next(); if let Some(&'{') = chars.peek() {
183 chars.next(); let param_content = collect_until_closing_brace(&mut chars);
186
187 if !param_content.is_empty() {
188 if param_content.starts_with('#') && param_content.len() > 1 {
190 let var_name = ¶m_content[1..];
191 if let Some(val) = shell_state.get_var(var_name) {
192 current.push_str(&val.len().to_string());
193 } else {
194 current.push('0');
195 }
196 } else {
197 match parse_parameter_expansion(¶m_content) {
199 Ok(expansion) => {
200 match expand_parameter(&expansion, shell_state) {
201 Ok(expanded) => {
202 current.push_str(&expanded);
203 }
204 Err(_) => {
205 current.push_str("${");
207 current.push_str(¶m_content);
208 current.push('}');
209 }
210 }
211 }
212 Err(_) => {
213 current.push_str("${");
215 current.push_str(¶m_content);
216 current.push('}');
217 }
218 }
219 }
220 } else {
221 current.push_str("${}");
223 }
224 } else if let Some(&'(') = chars.peek() {
225 current.push('$');
227 current.push('(');
228 chars.next();
229 } else if let Some(&'`') = chars.peek() {
230 current.push('$');
232 current.push('`');
233 chars.next();
234 } else {
235 let var_name = parse_variable_name(&mut chars);
237
238 if !var_name.is_empty() {
239 if let Some(val) = shell_state.get_var(&var_name) {
240 current.push_str(&val);
241 } else {
242 current.push('$');
243 current.push_str(&var_name);
244 }
245 } else {
246 current.push('$');
247 }
248 }
249 } else if ch == '`' {
250 current.push(ch);
252 chars.next();
253 } else {
254 current.push(ch);
255 chars.next();
256 }
257 }
258
259 if current.contains('$') {
261 let mut final_result = String::new();
263 let mut chars = current.chars().peekable();
264
265 while let Some(&ch) = chars.peek() {
266 if ch == '$' {
267 chars.next(); if let Some(&'{') = chars.peek() {
269 chars.next(); let param_content = collect_until_closing_brace(&mut chars);
272
273 if !param_content.is_empty() {
274 if param_content.starts_with('#') && param_content.len() > 1 {
276 let var_name = ¶m_content[1..];
277 if let Some(val) = shell_state.get_var(var_name) {
278 final_result.push_str(&val.len().to_string());
279 } else {
280 final_result.push('0');
281 }
282 } else {
283 match parse_parameter_expansion(¶m_content) {
285 Ok(expansion) => {
286 match expand_parameter(&expansion, shell_state) {
287 Ok(expanded) => {
288 if expanded.is_empty() {
289 } else {
293 final_result.push_str(&expanded);
294 }
295 }
296 Err(_) => {
297 final_result.push_str("${");
299 final_result.push_str(¶m_content);
300 final_result.push('}');
301 }
302 }
303 }
304 Err(_) => {
305 final_result.push_str("${");
307 final_result.push_str(¶m_content);
308 final_result.push('}');
309 }
310 }
311 }
312 } else {
313 final_result.push_str("${}");
315 }
316 } else {
317 let var_name = parse_variable_name(&mut chars);
318
319 if !var_name.is_empty() {
320 if let Some(val) = shell_state.get_var(&var_name) {
321 final_result.push_str(&val);
322 } else {
323 final_result.push('$');
324 final_result.push_str(&var_name);
325 }
326 } else {
327 final_result.push('$');
328 }
329 }
330 } else {
331 final_result.push(ch);
332 chars.next();
333 }
334 }
335 final_result
336 } else {
337 current
338 }
339}
340
341pub fn lex(input: &str, shell_state: &ShellState) -> Result<Vec<Token>, String> {
342 let mut tokens = Vec::new();
343 let mut chars = input.chars().peekable();
344 let mut current = String::new();
345 let mut in_double_quote = false;
346 let mut in_single_quote = false;
347
348 while let Some(&ch) = chars.peek() {
349 match ch {
350 ' ' | '\t' if !in_double_quote && !in_single_quote => {
351 flush_current_token(&mut current, &mut tokens);
352 chars.next();
353 }
354 '\n' if !in_double_quote && !in_single_quote => {
355 flush_current_token(&mut current, &mut tokens);
356 tokens.push(Token::Newline);
357 chars.next();
358 }
359 '"' if !in_single_quote => {
360 let is_escaped = current.ends_with('\\');
362
363 if is_escaped && in_double_quote {
364 current.pop(); current.push('"'); chars.next(); } else {
369 chars.next(); if in_double_quote {
371 in_double_quote = false;
375 } else {
376 in_double_quote = true;
379 }
380 }
381 }
382 '\\' if in_double_quote => {
383 chars.next(); if let Some(&next_ch) = chars.peek() {
386 if next_ch == '$'
388 || next_ch == '`'
389 || next_ch == '"'
390 || next_ch == '\\'
391 || next_ch == '\n'
392 {
393 current.push(next_ch);
395 chars.next(); } else {
397 current.push('\\');
399 current.push(next_ch);
400 chars.next();
401 }
402 } else {
403 current.push('\\');
405 }
406 }
407 '\'' => {
408 if in_single_quote {
409 in_single_quote = false;
413 } else if !in_double_quote {
414 in_single_quote = true;
417 }
418 chars.next();
419 }
420 '$' if !in_single_quote => {
421 chars.next(); if let Some(&'{') = chars.peek() {
423 chars.next(); let param_content = collect_until_closing_brace(&mut chars);
426
427 if !param_content.is_empty() {
428 if param_content.starts_with('#') && param_content.len() > 1 {
430 let var_name = ¶m_content[1..];
431 if let Some(val) = shell_state.get_var(var_name) {
432 current.push_str(&val.len().to_string());
433 } else {
434 current.push('0');
435 }
436 } else {
437 match parse_parameter_expansion(¶m_content) {
439 Ok(expansion) => {
440 match expand_parameter(&expansion, shell_state) {
441 Ok(expanded) => {
442 if expanded.is_empty() {
443 if !in_double_quote && !in_single_quote {
446 if !current.is_empty() {
448 if let Some(keyword) = is_keyword(¤t)
449 {
450 tokens.push(keyword);
451 } else {
452 let word = expand_variables_in_command(
453 ¤t,
454 shell_state,
455 );
456 tokens.push(Token::Word(word));
457 }
458 current.clear();
459 }
460 tokens.push(Token::Word("".to_string()));
462 }
463 } else {
465 current.push_str(&expanded);
466 }
467 }
468 Err(_) => {
469 if !current.is_empty() {
471 if let Some(keyword) = is_keyword(¤t) {
472 tokens.push(keyword);
473 } else {
474 let word = expand_variables_in_command(
475 ¤t,
476 shell_state,
477 );
478 tokens.push(Token::Word(word));
479 }
480 current.clear();
481 }
482 if let Some(space_pos) = param_content.find(' ') {
484 let first_part =
486 format!("${{{}}}", ¶m_content[..space_pos]);
487 let second_part = format!(
488 "{}}}",
489 ¶m_content[space_pos + 1..]
490 );
491 tokens.push(Token::Word(first_part));
492 tokens.push(Token::Word(second_part));
493 } else {
494 let literal = format!("${{{}}}", param_content);
495 tokens.push(Token::Word(literal));
496 }
497 }
498 }
499 }
500 Err(_) => {
501 current.push_str("${");
503 current.push_str(¶m_content);
504 current.push('}');
505 }
506 }
507 }
508 } else {
509 current.push_str("${}");
511 }
512 } else if let Some(&'(') = chars.peek() {
513 chars.next(); if let Some(&'(') = chars.peek() {
515 chars.next(); let arithmetic_expr = collect_with_paren_depth(&mut chars);
518 let found_closing = if let Some(&')') = chars.peek() {
520 chars.next(); true
522 } else {
523 false
524 };
525 current.push_str("$((");
527 current.push_str(&arithmetic_expr);
528 if found_closing {
529 current.push_str("))");
530 }
531 } else {
532 let sub_command = collect_with_paren_depth(&mut chars);
535 current.push_str("$(");
537 current.push_str(&sub_command);
538 current.push(')');
539 }
540 } else {
541 let var_name = parse_variable_name(&mut chars);
543
544 if !var_name.is_empty() {
545 current.push('$');
547 current.push_str(&var_name);
548 } else {
549 current.push('$');
550 }
551 }
552 }
553 '|' if !in_double_quote && !in_single_quote => {
554 flush_current_token(&mut current, &mut tokens);
555 chars.next(); if let Some(&'|') = chars.peek() {
558 chars.next(); tokens.push(Token::Or);
560 } else {
561 tokens.push(Token::Pipe);
562 }
563 skip_whitespace(&mut chars);
565 }
566 '&' if !in_double_quote && !in_single_quote => {
567 flush_current_token(&mut current, &mut tokens);
568 chars.next(); if let Some(&'&') = chars.peek() {
571 chars.next(); tokens.push(Token::And);
573 skip_whitespace(&mut chars);
575 } else {
576 current.push('&');
578 }
579 }
580 '>' if !in_double_quote && !in_single_quote => {
581 let is_fd_redirect = if !current.is_empty() {
584 current
585 .chars()
586 .last()
587 .map(|c| c.is_ascii_digit())
588 .unwrap_or(false)
589 } else {
590 false
591 };
592
593 if is_fd_redirect {
594 chars.next(); if let Some(&'&') = chars.peek() {
597 chars.next(); let mut target = String::new();
600 while let Some(&ch) = chars.peek() {
601 if ch.is_ascii_digit() || ch == '-' {
602 target.push(ch);
603 chars.next();
604 } else {
605 break;
606 }
607 }
608
609 if !target.is_empty() {
610 current.pop();
613
614 flush_current_token(&mut current, &mut tokens);
616
617 continue;
620 } else {
621 current.push('>');
623 current.push('&');
624 }
625 } else {
626 flush_current_token(&mut current, &mut tokens);
629
630 if let Some(&next_ch) = chars.peek() {
631 if next_ch == '>' {
632 chars.next();
633 tokens.push(Token::RedirAppend);
634 } else {
635 tokens.push(Token::RedirOut);
636 }
637 } else {
638 tokens.push(Token::RedirOut);
639 }
640 }
641 } else {
642 flush_current_token(&mut current, &mut tokens);
644 chars.next();
645 if let Some(&next_ch) = chars.peek() {
646 if next_ch == '>' {
647 chars.next();
648 tokens.push(Token::RedirAppend);
649 } else {
650 tokens.push(Token::RedirOut);
651 }
652 } else {
653 tokens.push(Token::RedirOut);
654 }
655 }
656 }
657 '<' if !in_double_quote && !in_single_quote => {
658 flush_current_token(&mut current, &mut tokens);
659 chars.next(); if let Some(&'<') = chars.peek() {
661 chars.next(); if let Some(&'<') = chars.peek() {
664 chars.next(); skip_whitespace(&mut chars);
667
668 let mut content = String::new();
669 let mut in_quote = false;
670 let mut quote_char = ' ';
671
672 while let Some(&ch) = chars.peek() {
673 if ch == '\n' && !in_quote {
674 break;
675 }
676 if (ch == '"' || ch == '\'') && !in_quote {
677 in_quote = true;
678 quote_char = ch;
679 chars.next(); } else if in_quote && ch == quote_char {
681 in_quote = false;
682 chars.next(); } else if !in_quote && (ch == ' ' || ch == '\t') {
684 break;
685 } else {
686 content.push(ch);
687 chars.next();
688 }
689 }
690
691 if !content.is_empty() {
692 tokens.push(Token::RedirHereString(content));
693 } else {
694 return Err("Invalid here-string syntax: expected content after <<<"
695 .to_string());
696 }
697 } else {
698 skip_whitespace(&mut chars);
700
701 let mut delimiter = String::new();
702 let mut in_quote = false;
703 let mut quote_char = ' ';
704 let mut was_quoted = false; while let Some(&ch) = chars.peek() {
707 if ch == '\n' && !in_quote {
708 break;
709 }
710 if (ch == '"' || ch == '\'') && !in_quote {
711 in_quote = true;
712 quote_char = ch;
713 was_quoted = true; chars.next(); } else if in_quote && ch == quote_char {
716 in_quote = false;
717 chars.next(); } else if !in_quote && (ch == ' ' || ch == '\t') {
719 break;
720 } else {
721 delimiter.push(ch);
722 chars.next();
723 }
724 }
725
726 if !delimiter.is_empty() {
727 tokens.push(Token::RedirHereDoc(delimiter, was_quoted));
729 } else {
730 return Err(
731 "Invalid here-document syntax: expected delimiter after <<"
732 .to_string(),
733 );
734 }
735 }
736 } else {
737 tokens.push(Token::RedirIn);
739 }
740 }
741 ')' if !in_double_quote && !in_single_quote => {
742 flush_current_token(&mut current, &mut tokens);
743 tokens.push(Token::RightParen);
744 chars.next();
745 }
746 '}' if !in_double_quote && !in_single_quote => {
747 flush_current_token(&mut current, &mut tokens);
748 tokens.push(Token::RightBrace);
749 chars.next();
750 }
751 '(' if !in_double_quote && !in_single_quote => {
752 flush_current_token(&mut current, &mut tokens);
753 tokens.push(Token::LeftParen);
754 chars.next();
755 }
756 '{' if !in_double_quote && !in_single_quote => {
757 let mut temp_chars = chars.clone();
759 let mut brace_content = String::new();
760 let mut depth = 1;
761
762 temp_chars.next(); while let Some(&ch) = temp_chars.peek() {
765 if ch == '{' {
766 depth += 1;
767 } else if ch == '}' {
768 depth -= 1;
769 if depth == 0 {
770 break;
771 }
772 }
773 brace_content.push(ch);
774 temp_chars.next();
775 }
776
777 if depth == 0 && !brace_content.trim().is_empty() {
778 if brace_content.contains(',') || brace_content.contains("..") {
781 current.push('{');
783 current.push_str(&brace_content);
784 current.push('}');
785 chars.next(); let mut content_depth = 1;
788 while let Some(&ch) = chars.peek() {
789 chars.next();
790 if ch == '{' {
791 content_depth += 1;
792 } else if ch == '}' {
793 content_depth -= 1;
794 if content_depth == 0 {
795 break;
796 }
797 }
798 }
799 } else {
800 flush_current_token(&mut current, &mut tokens);
802 tokens.push(Token::LeftBrace);
803 chars.next();
804 }
805 } else {
806 flush_current_token(&mut current, &mut tokens);
808 tokens.push(Token::LeftBrace);
809 chars.next();
810 }
811 }
812 '`' => {
813 flush_current_token(&mut current, &mut tokens);
814 chars.next();
815 let mut sub_command = String::new();
816 while let Some(&ch) = chars.peek() {
817 if ch == '`' {
818 chars.next();
819 break;
820 } else {
821 sub_command.push(ch);
822 chars.next();
823 }
824 }
825 current.push('`');
827 current.push_str(&sub_command);
828 current.push('`');
829 }
830 ';' if !in_double_quote && !in_single_quote => {
831 flush_current_token(&mut current, &mut tokens);
832 chars.next();
833 if let Some(&next_ch) = chars.peek() {
834 if next_ch == ';' {
835 chars.next();
836 tokens.push(Token::DoubleSemicolon);
837 } else {
838 tokens.push(Token::Semicolon);
839 }
840 } else {
841 tokens.push(Token::Semicolon);
842 }
843 }
844 _ => {
845 if ch == '~' && current.is_empty() {
846 if let Ok(home) = env::var("HOME") {
847 current.push_str(&home);
848 } else {
849 current.push('~');
850 }
851 } else {
852 current.push(ch);
853 }
854 chars.next();
855 }
856 }
857 }
858 flush_current_token(&mut current, &mut tokens);
859
860 Ok(tokens)
861}
862
863pub fn expand_aliases(
865 tokens: Vec<Token>,
866 shell_state: &ShellState,
867 expanded: &mut HashSet<String>,
868) -> Result<Vec<Token>, String> {
869 if tokens.is_empty() {
870 return Ok(tokens);
871 }
872
873 if let Token::Word(ref word) = tokens[0] {
875 if let Some(alias_value) = shell_state.get_alias(word) {
876 if expanded.contains(word) {
878 return Err(format!("Alias '{}' recursion detected", word));
879 }
880
881 expanded.insert(word.clone());
883
884 let alias_tokens = lex(alias_value, shell_state)?;
886
887 let expanded_alias_tokens = if !alias_tokens.is_empty() {
895 if let Token::Word(ref first_word) = alias_tokens[0] {
896 if first_word != word
898 && shell_state.get_alias(first_word).is_some()
899 && !expanded.contains(first_word)
900 {
901 expand_aliases(alias_tokens, shell_state, expanded)?
902 } else {
903 alias_tokens
904 }
905 } else {
906 alias_tokens
907 }
908 } else {
909 alias_tokens
910 };
911
912 expanded.remove(word);
914
915 let mut result = expanded_alias_tokens;
917 result.extend_from_slice(&tokens[1..]);
918 Ok(result)
919 } else {
920 Ok(tokens)
922 }
923 } else {
924 Ok(tokens)
926 }
927}
928
929#[cfg(test)]
930mod tests {
931 use super::*;
932
933 fn expand_tokens(tokens: Vec<Token>, shell_state: &mut ShellState) -> Vec<Token> {
936 let mut result = Vec::new();
937 for token in tokens {
938 match token {
939 Token::Word(word) => {
940 let expanded = crate::executor::expand_variables_in_string(&word, shell_state);
942 if !expanded.is_empty() || !word.starts_with("$(") {
945 result.push(Token::Word(expanded));
946 }
947 }
948 other => result.push(other),
949 }
950 }
951 result
952 }
953
954 #[test]
955 fn test_basic_word() {
956 let shell_state = ShellState::new();
957 let result = lex("ls", &shell_state).unwrap();
958 assert_eq!(result, vec![Token::Word("ls".to_string())]);
959 }
960
961 #[test]
962 fn test_multiple_words() {
963 let shell_state = ShellState::new();
964 let result = lex("ls -la", &shell_state).unwrap();
965 assert_eq!(
966 result,
967 vec![
968 Token::Word("ls".to_string()),
969 Token::Word("-la".to_string())
970 ]
971 );
972 }
973
974 #[test]
975 fn test_pipe() {
976 let shell_state = ShellState::new();
977 let result = lex("ls | grep txt", &shell_state).unwrap();
978 assert_eq!(
979 result,
980 vec![
981 Token::Word("ls".to_string()),
982 Token::Pipe,
983 Token::Word("grep".to_string()),
984 Token::Word("txt".to_string())
985 ]
986 );
987 }
988
989 #[test]
990 fn test_redirections() {
991 let shell_state = ShellState::new();
992 let result = lex("printf hello > output.txt", &shell_state).unwrap();
993 assert_eq!(
994 result,
995 vec![
996 Token::Word("printf".to_string()),
997 Token::Word("hello".to_string()),
998 Token::RedirOut,
999 Token::Word("output.txt".to_string())
1000 ]
1001 );
1002 }
1003
1004 #[test]
1005 fn test_append_redirection() {
1006 let shell_state = ShellState::new();
1007 let result = lex("printf hello >> output.txt", &shell_state).unwrap();
1008 assert_eq!(
1009 result,
1010 vec![
1011 Token::Word("printf".to_string()),
1012 Token::Word("hello".to_string()),
1013 Token::RedirAppend,
1014 Token::Word("output.txt".to_string())
1015 ]
1016 );
1017 }
1018
1019 #[test]
1020 fn test_input_redirection() {
1021 let shell_state = ShellState::new();
1022 let result = lex("cat < input.txt", &shell_state).unwrap();
1023 assert_eq!(
1024 result,
1025 vec![
1026 Token::Word("cat".to_string()),
1027 Token::RedirIn,
1028 Token::Word("input.txt".to_string())
1029 ]
1030 );
1031 }
1032
1033 #[test]
1034 fn test_double_quotes() {
1035 let shell_state = ShellState::new();
1036 let result = lex("echo \"hello world\"", &shell_state).unwrap();
1037 assert_eq!(
1038 result,
1039 vec![
1040 Token::Word("echo".to_string()),
1041 Token::Word("hello world".to_string())
1042 ]
1043 );
1044 }
1045
1046 #[test]
1047 fn test_single_quotes() {
1048 let shell_state = ShellState::new();
1049 let result = lex("echo 'hello world'", &shell_state).unwrap();
1050 assert_eq!(
1051 result,
1052 vec![
1053 Token::Word("echo".to_string()),
1054 Token::Word("hello world".to_string())
1055 ]
1056 );
1057 }
1058
1059 #[test]
1060 fn test_variable_expansion() {
1061 let mut shell_state = ShellState::new();
1062 shell_state.set_var("TEST_VAR", "expanded_value".to_string());
1063 let tokens = lex("echo $TEST_VAR", &shell_state).unwrap();
1064 let result = expand_tokens(tokens, &mut shell_state);
1065 assert_eq!(
1066 result,
1067 vec![
1068 Token::Word("echo".to_string()),
1069 Token::Word("expanded_value".to_string())
1070 ]
1071 );
1072 }
1073
1074 #[test]
1075 fn test_variable_expansion_nonexistent() {
1076 let shell_state = ShellState::new();
1077 let result = lex("echo $TEST_VAR2", &shell_state).unwrap();
1078 assert_eq!(
1079 result,
1080 vec![
1081 Token::Word("echo".to_string()),
1082 Token::Word("$TEST_VAR2".to_string())
1083 ]
1084 );
1085 }
1086
1087 #[test]
1088 fn test_empty_variable() {
1089 let shell_state = ShellState::new();
1090 let result = lex("echo $", &shell_state).unwrap();
1091 assert_eq!(
1092 result,
1093 vec![
1094 Token::Word("echo".to_string()),
1095 Token::Word("$".to_string())
1096 ]
1097 );
1098 }
1099
1100 #[test]
1101 fn test_mixed_quotes_and_variables() {
1102 let mut shell_state = ShellState::new();
1103 shell_state.set_var("USER", "alice".to_string());
1104 let tokens = lex("echo \"Hello $USER\"", &shell_state).unwrap();
1105 let result = expand_tokens(tokens, &mut shell_state);
1106 assert_eq!(
1107 result,
1108 vec![
1109 Token::Word("echo".to_string()),
1110 Token::Word("Hello alice".to_string())
1111 ]
1112 );
1113 }
1114
1115 #[test]
1116 fn test_unclosed_double_quote() {
1117 let shell_state = ShellState::new();
1119 let result = lex("echo \"hello", &shell_state).unwrap();
1120 assert_eq!(
1121 result,
1122 vec![
1123 Token::Word("echo".to_string()),
1124 Token::Word("hello".to_string())
1125 ]
1126 );
1127 }
1128
1129 #[test]
1130 fn test_empty_input() {
1131 let shell_state = ShellState::new();
1132 let result = lex("", &shell_state).unwrap();
1133 assert_eq!(result, Vec::<Token>::new());
1134 }
1135
1136 #[test]
1137 fn test_only_spaces() {
1138 let shell_state = ShellState::new();
1139 let result = lex(" ", &shell_state).unwrap();
1140 assert_eq!(result, Vec::<Token>::new());
1141 }
1142
1143 #[test]
1144 fn test_complex_pipeline() {
1145 let shell_state = ShellState::new();
1146 let result = lex(
1147 "cat input.txt | grep \"search term\" > output.txt",
1148 &shell_state,
1149 )
1150 .unwrap();
1151 assert_eq!(
1152 result,
1153 vec![
1154 Token::Word("cat".to_string()),
1155 Token::Word("input.txt".to_string()),
1156 Token::Pipe,
1157 Token::Word("grep".to_string()),
1158 Token::Word("search term".to_string()),
1159 Token::RedirOut,
1160 Token::Word("output.txt".to_string())
1161 ]
1162 );
1163 }
1164
1165 #[test]
1166 fn test_if_tokens() {
1167 let shell_state = ShellState::new();
1168 let result = lex("if true; then printf yes; fi", &shell_state).unwrap();
1169 assert_eq!(
1170 result,
1171 vec![
1172 Token::If,
1173 Token::Word("true".to_string()),
1174 Token::Semicolon,
1175 Token::Then,
1176 Token::Word("printf".to_string()),
1177 Token::Word("yes".to_string()),
1178 Token::Semicolon,
1179 Token::Fi,
1180 ]
1181 );
1182 }
1183
1184 #[test]
1185 fn test_command_substitution_dollar_paren() {
1186 let shell_state = ShellState::new();
1187 let result = lex("echo $(pwd)", &shell_state).unwrap();
1188 assert_eq!(result.len(), 2);
1190 assert_eq!(result[0], Token::Word("echo".to_string()));
1191 assert!(matches!(result[1], Token::Word(_)));
1192 }
1193
1194 #[test]
1195 fn test_command_substitution_backticks() {
1196 let shell_state = ShellState::new();
1197 let result = lex("echo `pwd`", &shell_state).unwrap();
1198 assert_eq!(result.len(), 2);
1200 assert_eq!(result[0], Token::Word("echo".to_string()));
1201 assert!(matches!(result[1], Token::Word(_)));
1202 }
1203
1204 #[test]
1205 fn test_command_substitution_with_arguments() {
1206 let mut shell_state = ShellState::new();
1207 let tokens = lex("echo $(echo hello world)", &shell_state).unwrap();
1208 let result = expand_tokens(tokens, &mut shell_state);
1209 assert_eq!(
1210 result,
1211 vec![
1212 Token::Word("echo".to_string()),
1213 Token::Word("hello world".to_string())
1214 ]
1215 );
1216 }
1217
1218 #[test]
1219 fn test_command_substitution_backticks_with_arguments() {
1220 let mut shell_state = ShellState::new();
1221 let tokens = lex("echo `echo hello world`", &shell_state).unwrap();
1222 let result = expand_tokens(tokens, &mut shell_state);
1223 assert_eq!(
1224 result,
1225 vec![
1226 Token::Word("echo".to_string()),
1227 Token::Word("hello world".to_string())
1228 ]
1229 );
1230 }
1231
1232 #[test]
1233 fn test_command_substitution_failure_fallback() {
1234 let shell_state = ShellState::new();
1235 let result = lex("echo $(nonexistent_command)", &shell_state).unwrap();
1236 assert_eq!(
1237 result,
1238 vec![
1239 Token::Word("echo".to_string()),
1240 Token::Word("$(nonexistent_command)".to_string())
1241 ]
1242 );
1243 }
1244
1245 #[test]
1246 fn test_command_substitution_backticks_failure_fallback() {
1247 let shell_state = ShellState::new();
1248 let result = lex("echo `nonexistent_command`", &shell_state).unwrap();
1249 assert_eq!(
1250 result,
1251 vec![
1252 Token::Word("echo".to_string()),
1253 Token::Word("`nonexistent_command`".to_string())
1254 ]
1255 );
1256 }
1257
1258 #[test]
1259 fn test_command_substitution_with_variables() {
1260 let mut shell_state = ShellState::new();
1261 shell_state.set_var("TEST_VAR", "test_value".to_string());
1262 let tokens = lex("echo $(echo $TEST_VAR)", &shell_state).unwrap();
1263 let result = expand_tokens(tokens, &mut shell_state);
1264 assert_eq!(
1265 result,
1266 vec![
1267 Token::Word("echo".to_string()),
1268 Token::Word("test_value".to_string())
1269 ]
1270 );
1271 }
1272
1273 #[test]
1274 fn test_command_substitution_in_assignment() {
1275 let mut shell_state = ShellState::new();
1276 let tokens = lex("MY_VAR=$(echo hello)", &shell_state).unwrap();
1277 let result = expand_tokens(tokens, &mut shell_state);
1278 assert_eq!(result, vec![Token::Word("MY_VAR=hello".to_string())]);
1280 }
1281
1282 #[test]
1283 fn test_command_substitution_backticks_in_assignment() {
1284 let mut shell_state = ShellState::new();
1285 let tokens = lex("MY_VAR=`echo hello`", &shell_state).unwrap();
1286 let result = expand_tokens(tokens, &mut shell_state);
1287 assert_eq!(
1289 result,
1290 vec![
1291 Token::Word("MY_VAR=".to_string()),
1292 Token::Word("hello".to_string())
1293 ]
1294 );
1295 }
1296
1297 #[test]
1298 fn test_command_substitution_with_quotes() {
1299 let mut shell_state = ShellState::new();
1300 let tokens = lex("echo \"$(echo hello world)\"", &shell_state).unwrap();
1301 let result = expand_tokens(tokens, &mut shell_state);
1302 assert_eq!(
1303 result,
1304 vec![
1305 Token::Word("echo".to_string()),
1306 Token::Word("hello world".to_string())
1307 ]
1308 );
1309 }
1310
1311 #[test]
1312 fn test_command_substitution_backticks_with_quotes() {
1313 let mut shell_state = ShellState::new();
1314 let tokens = lex("echo \"`echo hello world`\"", &shell_state).unwrap();
1315 let result = expand_tokens(tokens, &mut shell_state);
1316 assert_eq!(
1317 result,
1318 vec![
1319 Token::Word("echo".to_string()),
1320 Token::Word("hello world".to_string())
1321 ]
1322 );
1323 }
1324
1325 #[test]
1326 fn test_command_substitution_empty_output() {
1327 let mut shell_state = ShellState::new();
1328 let tokens = lex("echo $(true)", &shell_state).unwrap();
1329 let result = expand_tokens(tokens, &mut shell_state);
1330 assert_eq!(result, vec![Token::Word("echo".to_string())]);
1332 }
1333
1334 #[test]
1335 fn test_command_substitution_multiple_spaces() {
1336 let mut shell_state = ShellState::new();
1337 let tokens = lex("echo $(echo 'hello world')", &shell_state).unwrap();
1338 let result = expand_tokens(tokens, &mut shell_state);
1339 assert_eq!(
1340 result,
1341 vec![
1342 Token::Word("echo".to_string()),
1343 Token::Word("hello world".to_string())
1344 ]
1345 );
1346 }
1347
1348 #[test]
1349 fn test_command_substitution_with_newlines() {
1350 let mut shell_state = ShellState::new();
1351 let tokens = lex("echo $(printf 'hello\nworld')", &shell_state).unwrap();
1352 let result = expand_tokens(tokens, &mut shell_state);
1353 assert_eq!(
1354 result,
1355 vec![
1356 Token::Word("echo".to_string()),
1357 Token::Word("hello\nworld".to_string())
1358 ]
1359 );
1360 }
1361
1362 #[test]
1363 fn test_command_substitution_special_characters() {
1364 let shell_state = ShellState::new();
1365 let result = lex("echo $(echo '$#@^&*()')", &shell_state).unwrap();
1366 println!("Special chars test result: {:?}", result);
1367 assert_eq!(result.len(), 2);
1370 assert_eq!(result[0], Token::Word("echo".to_string()));
1371 assert!(matches!(result[1], Token::Word(_)));
1372 }
1373
1374 #[test]
1375 fn test_nested_command_substitution() {
1376 let shell_state = ShellState::new();
1379 let result = lex("echo $(echo $(pwd))", &shell_state).unwrap();
1380 assert_eq!(result.len(), 2);
1382 assert_eq!(result[0], Token::Word("echo".to_string()));
1383 assert!(matches!(result[1], Token::Word(_)));
1384 }
1385
1386 #[test]
1387 fn test_command_substitution_in_pipeline() {
1388 let shell_state = ShellState::new();
1389 let result = lex("$(echo hello) | cat", &shell_state).unwrap();
1390 println!("Pipeline test result: {:?}", result);
1391 assert_eq!(result.len(), 3);
1392 assert!(matches!(result[0], Token::Word(_)));
1393 assert_eq!(result[1], Token::Pipe);
1394 assert_eq!(result[2], Token::Word("cat".to_string()));
1395 }
1396
1397 #[test]
1398 fn test_command_substitution_with_redirection() {
1399 let shell_state = ShellState::new();
1400 let result = lex("$(echo hello) > output.txt", &shell_state).unwrap();
1401 assert_eq!(result.len(), 3);
1402 assert!(matches!(result[0], Token::Word(_)));
1403 assert_eq!(result[1], Token::RedirOut);
1404 assert_eq!(result[2], Token::Word("output.txt".to_string()));
1405 }
1406
1407 #[test]
1408 fn test_variable_in_quotes_with_pipe() {
1409 let mut shell_state = ShellState::new();
1410 shell_state.set_var("PATH", "/usr/bin:/bin".to_string());
1411 let tokens = lex("echo \"$PATH\" | tr ':' '\\n'", &shell_state).unwrap();
1412 let result = expand_tokens(tokens, &mut shell_state);
1413 assert_eq!(
1414 result,
1415 vec![
1416 Token::Word("echo".to_string()),
1417 Token::Word("/usr/bin:/bin".to_string()),
1418 Token::Pipe,
1419 Token::Word("tr".to_string()),
1420 Token::Word(":".to_string()),
1421 Token::Word("\\n".to_string())
1422 ]
1423 );
1424 }
1425
1426 #[test]
1427 fn test_expand_aliases_simple() {
1428 let mut shell_state = ShellState::new();
1429 shell_state.set_alias("ll", "ls -l".to_string());
1430 let tokens = vec![Token::Word("ll".to_string())];
1431 let result = expand_aliases(tokens, &shell_state, &mut HashSet::new()).unwrap();
1432 assert_eq!(
1433 result,
1434 vec![Token::Word("ls".to_string()), Token::Word("-l".to_string())]
1435 );
1436 }
1437
1438 #[test]
1439 fn test_expand_aliases_with_args() {
1440 let mut shell_state = ShellState::new();
1441 shell_state.set_alias("ll", "ls -l".to_string());
1442 let tokens = vec![
1443 Token::Word("ll".to_string()),
1444 Token::Word("/tmp".to_string()),
1445 ];
1446 let result = expand_aliases(tokens, &shell_state, &mut HashSet::new()).unwrap();
1447 assert_eq!(
1448 result,
1449 vec![
1450 Token::Word("ls".to_string()),
1451 Token::Word("-l".to_string()),
1452 Token::Word("/tmp".to_string())
1453 ]
1454 );
1455 }
1456
1457 #[test]
1458 fn test_expand_aliases_no_alias() {
1459 let shell_state = ShellState::new();
1460 let tokens = vec![Token::Word("ls".to_string())];
1461 let result = expand_aliases(tokens.clone(), &shell_state, &mut HashSet::new()).unwrap();
1462 assert_eq!(result, tokens);
1463 }
1464
1465 #[test]
1466 fn test_expand_aliases_chained() {
1467 let mut shell_state = ShellState::new();
1471 shell_state.set_alias("a", "b".to_string());
1472 shell_state.set_alias("b", "a".to_string());
1473 let tokens = vec![Token::Word("a".to_string())];
1474 let result = expand_aliases(tokens, &shell_state, &mut HashSet::new());
1475 assert!(result.is_ok());
1477 assert_eq!(result.unwrap(), vec![Token::Word("a".to_string())]);
1478 }
1479
1480 #[test]
1481 fn test_arithmetic_expansion_simple() {
1482 let mut shell_state = ShellState::new();
1483 let tokens = lex("echo $((2 + 3))", &shell_state).unwrap();
1484 let result = expand_tokens(tokens, &mut shell_state);
1485 assert_eq!(
1486 result,
1487 vec![
1488 Token::Word("echo".to_string()),
1489 Token::Word("5".to_string())
1490 ]
1491 );
1492 }
1493
1494 #[test]
1495 fn test_arithmetic_expansion_with_variables() {
1496 let mut shell_state = ShellState::new();
1497 shell_state.set_var("x", "10".to_string());
1498 shell_state.set_var("y", "20".to_string());
1499 let tokens = lex("echo $((x + y * 2))", &shell_state).unwrap();
1500 let result = expand_tokens(tokens, &mut shell_state);
1501 assert_eq!(
1502 result,
1503 vec![
1504 Token::Word("echo".to_string()),
1505 Token::Word("50".to_string()) ]
1507 );
1508 }
1509
1510 #[test]
1511 fn test_arithmetic_expansion_comparison() {
1512 let mut shell_state = ShellState::new();
1513 let tokens = lex("echo $((5 > 3))", &shell_state).unwrap();
1514 let result = expand_tokens(tokens, &mut shell_state);
1515 assert_eq!(
1516 result,
1517 vec![
1518 Token::Word("echo".to_string()),
1519 Token::Word("1".to_string()) ]
1521 );
1522 }
1523
1524 #[test]
1525 fn test_arithmetic_expansion_complex() {
1526 let mut shell_state = ShellState::new();
1527 shell_state.set_var("a", "3".to_string());
1528 let tokens = lex("echo $((a * 2 + 5))", &shell_state).unwrap();
1529 let result = expand_tokens(tokens, &mut shell_state);
1530 assert_eq!(
1531 result,
1532 vec![
1533 Token::Word("echo".to_string()),
1534 Token::Word("11".to_string()) ]
1536 );
1537 }
1538
1539 #[test]
1540 fn test_arithmetic_expansion_unmatched_parentheses() {
1541 let mut shell_state = ShellState::new();
1542 let tokens = lex("echo $((2 + 3", &shell_state).unwrap();
1543 let result = expand_tokens(tokens, &mut shell_state);
1544 assert_eq!(result.len(), 2);
1546 assert_eq!(result[0], Token::Word("echo".to_string()));
1547 let second_token = &result[1];
1549 if let Token::Word(s) = second_token {
1550 assert!(
1551 s.starts_with("$((") && s.contains("2") && s.contains("3"),
1552 "Expected unmatched arithmetic to be kept as literal, got: {}",
1553 s
1554 );
1555 } else {
1556 panic!("Expected Word token");
1557 }
1558 }
1559
1560 #[test]
1561 fn test_arithmetic_expansion_division_by_zero() {
1562 let mut shell_state = ShellState::new();
1563 let tokens = lex("echo $((5 / 0))", &shell_state).unwrap();
1564 let result = expand_tokens(tokens, &mut shell_state);
1565 assert_eq!(result.len(), 2);
1567 assert_eq!(result[0], Token::Word("echo".to_string()));
1568 if let Token::Word(s) = &result[1] {
1570 assert!(
1571 s.contains("Division by zero"),
1572 "Expected division by zero error, got: {}",
1573 s
1574 );
1575 } else {
1576 panic!("Expected Word token");
1577 }
1578 }
1579
1580 #[test]
1581 fn test_parameter_expansion_simple() {
1582 let mut shell_state = ShellState::new();
1583 shell_state.set_var("TEST_VAR", "hello world".to_string());
1584 let result = lex("echo ${TEST_VAR}", &shell_state).unwrap();
1585 assert_eq!(
1586 result,
1587 vec![
1588 Token::Word("echo".to_string()),
1589 Token::Word("hello world".to_string())
1590 ]
1591 );
1592 }
1593
1594 #[test]
1595 fn test_parameter_expansion_unset_variable() {
1596 let shell_state = ShellState::new();
1597 let result = lex("echo ${UNSET_VAR}", &shell_state).unwrap();
1598 assert_eq!(
1599 result,
1600 vec![Token::Word("echo".to_string()), Token::Word("".to_string())]
1601 );
1602 }
1603
1604 #[test]
1605 fn test_parameter_expansion_default() {
1606 let shell_state = ShellState::new();
1607 let result = lex("echo ${UNSET_VAR:-default}", &shell_state).unwrap();
1608 assert_eq!(
1609 result,
1610 vec![
1611 Token::Word("echo".to_string()),
1612 Token::Word("default".to_string())
1613 ]
1614 );
1615 }
1616
1617 #[test]
1618 fn test_parameter_expansion_default_set_variable() {
1619 let mut shell_state = ShellState::new();
1620 shell_state.set_var("TEST_VAR", "value".to_string());
1621 let result = lex("echo ${TEST_VAR:-default}", &shell_state).unwrap();
1622 assert_eq!(
1623 result,
1624 vec![
1625 Token::Word("echo".to_string()),
1626 Token::Word("value".to_string())
1627 ]
1628 );
1629 }
1630
1631 #[test]
1632 fn test_parameter_expansion_assign_default() {
1633 let shell_state = ShellState::new();
1634 let result = lex("echo ${UNSET_VAR:=default}", &shell_state).unwrap();
1635 assert_eq!(
1636 result,
1637 vec![
1638 Token::Word("echo".to_string()),
1639 Token::Word("default".to_string())
1640 ]
1641 );
1642 }
1643
1644 #[test]
1645 fn test_parameter_expansion_alternative() {
1646 let mut shell_state = ShellState::new();
1647 shell_state.set_var("TEST_VAR", "value".to_string());
1648 let result = lex("echo ${TEST_VAR:+replacement}", &shell_state).unwrap();
1649 assert_eq!(
1650 result,
1651 vec![
1652 Token::Word("echo".to_string()),
1653 Token::Word("replacement".to_string())
1654 ]
1655 );
1656 }
1657
1658 #[test]
1659 fn test_parameter_expansion_alternative_unset() {
1660 let shell_state = ShellState::new();
1661 let result = lex("echo ${UNSET_VAR:+replacement}", &shell_state).unwrap();
1662 assert_eq!(
1663 result,
1664 vec![Token::Word("echo".to_string()), Token::Word("".to_string())]
1665 );
1666 }
1667
1668 #[test]
1669 fn test_parameter_expansion_substring() {
1670 let mut shell_state = ShellState::new();
1671 shell_state.set_var("TEST_VAR", "hello world".to_string());
1672 let result = lex("echo ${TEST_VAR:6}", &shell_state).unwrap();
1673 assert_eq!(
1674 result,
1675 vec![
1676 Token::Word("echo".to_string()),
1677 Token::Word("world".to_string())
1678 ]
1679 );
1680 }
1681
1682 #[test]
1683 fn test_parameter_expansion_substring_with_length() {
1684 let mut shell_state = ShellState::new();
1685 shell_state.set_var("TEST_VAR", "hello world".to_string());
1686 let result = lex("echo ${TEST_VAR:0:5}", &shell_state).unwrap();
1687 assert_eq!(
1688 result,
1689 vec![
1690 Token::Word("echo".to_string()),
1691 Token::Word("hello".to_string())
1692 ]
1693 );
1694 }
1695
1696 #[test]
1697 fn test_parameter_expansion_length() {
1698 let mut shell_state = ShellState::new();
1699 shell_state.set_var("TEST_VAR", "hello".to_string());
1700 let result = lex("echo ${#TEST_VAR}", &shell_state).unwrap();
1701 assert_eq!(
1702 result,
1703 vec![
1704 Token::Word("echo".to_string()),
1705 Token::Word("5".to_string())
1706 ]
1707 );
1708 }
1709
1710 #[test]
1711 fn test_parameter_expansion_remove_shortest_prefix() {
1712 let mut shell_state = ShellState::new();
1713 shell_state.set_var("TEST_VAR", "prefix_hello".to_string());
1714 let result = lex("echo ${TEST_VAR#prefix_}", &shell_state).unwrap();
1715 assert_eq!(
1716 result,
1717 vec![
1718 Token::Word("echo".to_string()),
1719 Token::Word("hello".to_string())
1720 ]
1721 );
1722 }
1723
1724 #[test]
1725 fn test_parameter_expansion_remove_longest_prefix() {
1726 let mut shell_state = ShellState::new();
1727 shell_state.set_var("TEST_VAR", "prefix_prefix_hello".to_string());
1728 let result = lex("echo ${TEST_VAR##prefix_}", &shell_state).unwrap();
1729 assert_eq!(
1730 result,
1731 vec![
1732 Token::Word("echo".to_string()),
1733 Token::Word("prefix_hello".to_string())
1734 ]
1735 );
1736 }
1737
1738 #[test]
1739 fn test_parameter_expansion_remove_shortest_suffix() {
1740 let mut shell_state = ShellState::new();
1741 shell_state.set_var("TEST_VAR", "hello_suffix".to_string());
1742 let result = lex("echo ${TEST_VAR%suffix}", &shell_state).unwrap();
1743 assert_eq!(
1744 result,
1745 vec![
1746 Token::Word("echo".to_string()),
1747 Token::Word("hello_".to_string()) ]
1749 );
1750 }
1751
1752 #[test]
1753 fn test_parameter_expansion_remove_longest_suffix() {
1754 let mut shell_state = ShellState::new();
1755 shell_state.set_var("TEST_VAR", "hello_suffix_suffix".to_string());
1756 let result = lex("echo ${TEST_VAR%%suffix}", &shell_state).unwrap();
1757 assert_eq!(
1758 result,
1759 vec![
1760 Token::Word("echo".to_string()),
1761 Token::Word("hello_suffix_".to_string()) ]
1763 );
1764 }
1765
1766 #[test]
1767 fn test_parameter_expansion_substitute() {
1768 let mut shell_state = ShellState::new();
1769 shell_state.set_var("TEST_VAR", "hello world".to_string());
1770 let result = lex("echo ${TEST_VAR/world/universe}", &shell_state).unwrap();
1771 assert_eq!(
1772 result,
1773 vec![
1774 Token::Word("echo".to_string()),
1775 Token::Word("hello universe".to_string())
1776 ]
1777 );
1778 }
1779
1780 #[test]
1781 fn test_parameter_expansion_substitute_all() {
1782 let mut shell_state = ShellState::new();
1783 shell_state.set_var("TEST_VAR", "hello world world".to_string());
1784 let result = lex("echo ${TEST_VAR//world/universe}", &shell_state).unwrap();
1785 assert_eq!(
1786 result,
1787 vec![
1788 Token::Word("echo".to_string()),
1789 Token::Word("hello universe universe".to_string())
1790 ]
1791 );
1792 }
1793
1794 #[test]
1795 fn test_parameter_expansion_mixed_with_regular_variables() {
1796 let mut shell_state = ShellState::new();
1797 shell_state.set_var("VAR1", "value1".to_string());
1798 shell_state.set_var("VAR2", "value2".to_string());
1799 let tokens = lex("echo $VAR1 and ${VAR2}", &shell_state).unwrap();
1800 let result = expand_tokens(tokens, &mut shell_state);
1801 assert_eq!(
1802 result,
1803 vec![
1804 Token::Word("echo".to_string()),
1805 Token::Word("value1".to_string()),
1806 Token::Word("and".to_string()),
1807 Token::Word("value2".to_string())
1808 ]
1809 );
1810 }
1811
1812 #[test]
1813 fn test_parameter_expansion_in_double_quotes() {
1814 let mut shell_state = ShellState::new();
1815 shell_state.set_var("TEST_VAR", "hello".to_string());
1816 let result = lex("echo \"Value: ${TEST_VAR}\"", &shell_state).unwrap();
1817 assert_eq!(
1818 result,
1819 vec![
1820 Token::Word("echo".to_string()),
1821 Token::Word("Value: hello".to_string())
1822 ]
1823 );
1824 }
1825
1826 #[test]
1827 fn test_parameter_expansion_error_unset() {
1828 let shell_state = ShellState::new();
1829 let result = lex("echo ${UNSET_VAR:?error message}", &shell_state);
1830 assert!(result.is_ok());
1832 let tokens = result.unwrap();
1833 assert_eq!(tokens.len(), 3);
1834 assert_eq!(tokens[0], Token::Word("echo".to_string()));
1835 assert_eq!(tokens[1], Token::Word("${UNSET_VAR:?error}".to_string()));
1836 assert_eq!(tokens[2], Token::Word("message}".to_string()));
1837 }
1838
1839 #[test]
1840 fn test_parameter_expansion_complex_expression() {
1841 let mut shell_state = ShellState::new();
1842 shell_state.set_var("PATH", "/usr/bin:/bin:/usr/local/bin".to_string());
1843 let result = lex("echo ${PATH#/usr/bin:}", &shell_state).unwrap();
1844 assert_eq!(
1845 result,
1846 vec![
1847 Token::Word("echo".to_string()),
1848 Token::Word("/bin:/usr/local/bin".to_string())
1849 ]
1850 );
1851 }
1852
1853 #[test]
1854 fn test_local_keyword() {
1855 let shell_state = ShellState::new();
1856 let result = lex("local myvar", &shell_state).unwrap();
1857 assert_eq!(result, vec![Token::Local, Token::Word("myvar".to_string())]);
1858 }
1859
1860 #[test]
1861 fn test_local_keyword_in_function() {
1862 let shell_state = ShellState::new();
1863 let result = lex("local var=value", &shell_state).unwrap();
1864 assert_eq!(
1865 result,
1866 vec![Token::Local, Token::Word("var=value".to_string())]
1867 );
1868 }
1869
1870 #[test]
1871 fn test_single_quotes_with_semicolons() {
1872 let shell_state = ShellState::new();
1874 let result = lex("trap 'echo \"A\"; echo \"B\"' EXIT", &shell_state).unwrap();
1875 assert_eq!(
1876 result,
1877 vec![
1878 Token::Word("trap".to_string()),
1879 Token::Word("echo \"A\"; echo \"B\"".to_string()),
1880 Token::Word("EXIT".to_string())
1881 ]
1882 );
1883 }
1884
1885 #[test]
1886 fn test_double_quotes_with_semicolons() {
1887 let shell_state = ShellState::new();
1889 let result = lex("echo \"command1; command2\"", &shell_state).unwrap();
1890 assert_eq!(
1891 result,
1892 vec![
1893 Token::Word("echo".to_string()),
1894 Token::Word("command1; command2".to_string())
1895 ]
1896 );
1897 }
1898
1899 #[test]
1900 fn test_semicolons_outside_quotes() {
1901 let shell_state = ShellState::new();
1903 let result = lex("echo hello; echo world", &shell_state).unwrap();
1904 assert_eq!(
1905 result,
1906 vec![
1907 Token::Word("echo".to_string()),
1908 Token::Word("hello".to_string()),
1909 Token::Semicolon,
1910 Token::Word("echo".to_string()),
1911 Token::Word("world".to_string())
1912 ]
1913 );
1914 }
1915
1916 #[test]
1917 fn test_here_document_redirection() {
1918 let shell_state = ShellState::new();
1919 let result = lex("cat << EOF", &shell_state).unwrap();
1920 assert_eq!(
1921 result,
1922 vec![
1923 Token::Word("cat".to_string()),
1924 Token::RedirHereDoc("EOF".to_string(), false)
1925 ]
1926 );
1927 }
1928
1929 #[test]
1930 fn test_here_string_redirection() {
1931 let shell_state = ShellState::new();
1932 let result = lex("cat <<< \"hello world\"", &shell_state).unwrap();
1933 assert_eq!(
1934 result,
1935 vec![
1936 Token::Word("cat".to_string()),
1937 Token::RedirHereString("hello world".to_string())
1938 ]
1939 );
1940 }
1941
1942 #[test]
1943 fn test_here_document_with_quoted_delimiter() {
1944 let shell_state = ShellState::new();
1945 let result = lex("command << 'EOF'", &shell_state).unwrap();
1946 assert_eq!(
1947 result,
1948 vec![
1949 Token::Word("command".to_string()),
1950 Token::RedirHereDoc("EOF".to_string(), true) ]
1952 );
1953 }
1954
1955 #[test]
1956 fn test_here_string_without_quotes() {
1957 let shell_state = ShellState::new();
1958 let result = lex("grep <<< pattern", &shell_state).unwrap();
1959 assert_eq!(
1960 result,
1961 vec![
1962 Token::Word("grep".to_string()),
1963 Token::RedirHereString("pattern".to_string())
1964 ]
1965 );
1966 }
1967
1968 #[test]
1969 fn test_redirections_mixed() {
1970 let shell_state = ShellState::new();
1971 let result = lex(
1972 "cat < input.txt <<< \"fallback\" > output.txt",
1973 &shell_state,
1974 )
1975 .unwrap();
1976 assert_eq!(
1977 result,
1978 vec![
1979 Token::Word("cat".to_string()),
1980 Token::RedirIn,
1981 Token::Word("input.txt".to_string()),
1982 Token::RedirHereString("fallback".to_string()),
1983 Token::RedirOut,
1984 Token::Word("output.txt".to_string())
1985 ]
1986 );
1987 }
1988}