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