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 tokens.push(Token::Word(current.clone()));
346 current.clear();
347 in_double_quote = false;
348 } else {
349 if !current.is_empty() {
351 if let Some(keyword) = is_keyword(¤t) {
352 tokens.push(keyword);
353 } else {
354 tokens.push(Token::Word(current.clone()));
356 }
357 current.clear();
358 }
359 in_double_quote = true;
360 }
361 }
362 }
363 '\'' => {
364 if in_single_quote {
365 tokens.push(Token::Word(current.clone()));
367 current.clear();
368 in_single_quote = false;
369 } else if !in_double_quote {
370 in_single_quote = true;
372 }
373 chars.next();
374 }
375 '$' if !in_single_quote => {
376 chars.next(); if let Some(&'{') = chars.peek() {
378 chars.next(); let mut param_content = String::new();
381
382 while let Some(&ch) = chars.peek() {
384 if ch == '}' {
385 chars.next(); break;
387 } else {
388 param_content.push(ch);
389 chars.next();
390 }
391 }
392
393 if !param_content.is_empty() {
394 if param_content.starts_with('#') && param_content.len() > 1 {
396 let var_name = ¶m_content[1..];
397 if let Some(val) = shell_state.get_var(var_name) {
398 current.push_str(&val.len().to_string());
399 } else {
400 current.push('0');
401 }
402 } else {
403 match parse_parameter_expansion(¶m_content) {
405 Ok(expansion) => {
406 match expand_parameter(&expansion, shell_state) {
407 Ok(expanded) => {
408 if expanded.is_empty() {
409 if !current.is_empty() {
411 if let Some(keyword) = is_keyword(¤t) {
412 tokens.push(keyword);
413 } else {
414 let word = expand_variables_in_command(¤t, shell_state);
415 tokens.push(Token::Word(word));
416 }
417 current.clear();
418 }
419 tokens.push(Token::Word("".to_string()));
421 } else {
422 current.push_str(&expanded);
423 }
424 }
425 Err(_) => {
426 if !current.is_empty() {
428 if let Some(keyword) = is_keyword(¤t) {
429 tokens.push(keyword);
430 } else {
431 let word = expand_variables_in_command(¤t, shell_state);
432 tokens.push(Token::Word(word));
433 }
434 current.clear();
435 }
436 if let Some(space_pos) = param_content.find(' ') {
438 let first_part = format!("${{{}}}", ¶m_content[..space_pos]);
440 let second_part = format!("{}}}", ¶m_content[space_pos + 1..]);
441 tokens.push(Token::Word(first_part));
442 tokens.push(Token::Word(second_part));
443 } else {
444 let literal = format!("${{{}}}", param_content);
445 tokens.push(Token::Word(literal));
446 }
447 }
448 }
449 }
450 Err(_) => {
451 current.push_str("${");
453 current.push_str(¶m_content);
454 current.push('}');
455 }
456 }
457 }
458 } else {
459 current.push_str("${}");
461 }
462 } else if let Some(&'(') = chars.peek() {
463 chars.next(); if let Some(&'(') = chars.peek() {
465 chars.next(); let mut arithmetic_expr = String::new();
468 let mut paren_depth = 1;
469 let mut found_closing = false;
470 while let Some(&ch) = chars.peek() {
471 if ch == '(' {
472 paren_depth += 1;
473 arithmetic_expr.push(ch);
474 chars.next();
475 } else if ch == ')' {
476 paren_depth -= 1;
477 if paren_depth == 0 {
478 chars.next(); if let Some(&')') = chars.peek() {
481 chars.next(); found_closing = true;
483 }
484 break;
485 } else {
486 arithmetic_expr.push(ch);
487 chars.next();
488 }
489 } else {
490 arithmetic_expr.push(ch);
491 chars.next();
492 }
493 }
494 current.push_str("$((");
496 current.push_str(&arithmetic_expr);
497 if found_closing {
498 current.push_str("))");
499 }
500 } else {
501 let mut sub_command = String::new();
504 let mut paren_depth = 1;
505 while let Some(&ch) = chars.peek() {
506 if ch == '(' {
507 paren_depth += 1;
508 sub_command.push(ch);
509 chars.next();
510 } else if ch == ')' {
511 paren_depth -= 1;
512 if paren_depth == 0 {
513 chars.next(); break;
515 } else {
516 sub_command.push(ch);
517 chars.next();
518 }
519 } else {
520 sub_command.push(ch);
521 chars.next();
522 }
523 }
524 current.push_str("$(");
526 current.push_str(&sub_command);
527 current.push(')');
528 }
529 } else {
530 let mut var_name = String::new();
532
533 if let Some(&ch) = chars.peek() {
535 if ch == '?' || ch == '$' || ch.is_ascii_digit() {
536 var_name.push(ch);
538 chars.next();
539 } else if ch == '#' || ch == '@' || ch == '*' || ch == '!' {
540 var_name.push(ch);
542 chars.next();
543 } else {
544 while let Some(&ch) = chars.peek() {
546 if ch.is_alphanumeric() || ch == '_' {
547 var_name.push(ch);
548 chars.next();
549 } else {
550 break;
551 }
552 }
553 }
554 }
555
556 if !var_name.is_empty() {
557 current.push('$');
559 current.push_str(&var_name);
560 } else {
561 current.push('$');
562 }
563 }
564 }
565 '|' if !in_double_quote && !in_single_quote => {
566 if !current.is_empty() {
567 if let Some(keyword) = is_keyword(¤t) {
568 tokens.push(keyword);
569 } else {
570 tokens.push(Token::Word(current.clone()));
572 }
573 current.clear();
574 }
575 chars.next(); if let Some(&'|') = chars.peek() {
578 chars.next(); tokens.push(Token::Or);
580 } else {
581 tokens.push(Token::Pipe);
582 }
583 while let Some(&ch) = chars.peek() {
585 if ch == ' ' || ch == '\t' {
586 chars.next();
587 } else {
588 break;
589 }
590 }
591 }
592 '&' if !in_double_quote && !in_single_quote => {
593 if !current.is_empty() {
594 if let Some(keyword) = is_keyword(¤t) {
595 tokens.push(keyword);
596 } else {
597 tokens.push(Token::Word(current.clone()));
598 }
599 current.clear();
600 }
601 chars.next(); if let Some(&'&') = chars.peek() {
604 chars.next(); tokens.push(Token::And);
606 while let Some(&ch) = chars.peek() {
608 if ch == ' ' || ch == '\t' {
609 chars.next();
610 } else {
611 break;
612 }
613 }
614 } else {
615 current.push('&');
617 }
618 }
619 '>' if !in_double_quote && !in_single_quote => {
620 let is_fd_redirect = if !current.is_empty() {
623 current.chars().last().map(|c| c.is_ascii_digit()).unwrap_or(false)
624 } else {
625 false
626 };
627
628 if is_fd_redirect {
629 chars.next(); if let Some(&'&') = chars.peek() {
632 chars.next(); let mut target = String::new();
635 while let Some(&ch) = chars.peek() {
636 if ch.is_ascii_digit() || ch == '-' {
637 target.push(ch);
638 chars.next();
639 } else {
640 break;
641 }
642 }
643
644 if !target.is_empty() {
645 current.pop();
648
649 if !current.is_empty() {
651 if let Some(keyword) = is_keyword(¤t) {
652 tokens.push(keyword);
653 } else {
654 tokens.push(Token::Word(current.clone()));
655 }
656 current.clear();
657 }
658
659 continue;
662 } else {
663 current.push('>');
665 current.push('&');
666 }
667 } else {
668 if !current.is_empty() {
671 if let Some(keyword) = is_keyword(¤t) {
672 tokens.push(keyword);
673 } else {
674 tokens.push(Token::Word(current.clone()));
675 }
676 current.clear();
677 }
678
679 if let Some(&next_ch) = chars.peek() {
680 if next_ch == '>' {
681 chars.next();
682 tokens.push(Token::RedirAppend);
683 } else {
684 tokens.push(Token::RedirOut);
685 }
686 } else {
687 tokens.push(Token::RedirOut);
688 }
689 }
690 } else {
691 if !current.is_empty() {
693 if let Some(keyword) = is_keyword(¤t) {
694 tokens.push(keyword);
695 } else {
696 tokens.push(Token::Word(current.clone()));
698 }
699 current.clear();
700 }
701 chars.next();
702 if let Some(&next_ch) = chars.peek() {
703 if next_ch == '>' {
704 chars.next();
705 tokens.push(Token::RedirAppend);
706 } else {
707 tokens.push(Token::RedirOut);
708 }
709 } else {
710 tokens.push(Token::RedirOut);
711 }
712 }
713 }
714 '<' if !in_double_quote && !in_single_quote => {
715 if !current.is_empty() {
716 if let Some(keyword) = is_keyword(¤t) {
717 tokens.push(keyword);
718 } else {
719 tokens.push(Token::Word(current.clone()));
721 }
722 current.clear();
723 }
724 tokens.push(Token::RedirIn);
725 chars.next();
726 }
727 ')' if !in_double_quote && !in_single_quote => {
728 if !current.is_empty() {
729 if let Some(keyword) = is_keyword(¤t) {
730 tokens.push(keyword);
731 } else {
732 tokens.push(Token::Word(current.clone()));
734 }
735 current.clear();
736 }
737 tokens.push(Token::RightParen);
738 chars.next();
739 }
740 '}' if !in_double_quote && !in_single_quote => {
741 if !current.is_empty() {
742 if let Some(keyword) = is_keyword(¤t) {
743 tokens.push(keyword);
744 } else {
745 tokens.push(Token::Word(current.clone()));
747 }
748 current.clear();
749 }
750 tokens.push(Token::RightBrace);
751 chars.next();
752 }
753 '(' if !in_double_quote && !in_single_quote => {
754 if !current.is_empty() {
755 if let Some(keyword) = is_keyword(¤t) {
756 tokens.push(keyword);
757 } else {
758 tokens.push(Token::Word(current.clone()));
760 }
761 current.clear();
762 }
763 tokens.push(Token::LeftParen);
764 chars.next();
765 }
766 '{' if !in_double_quote && !in_single_quote => {
767 let mut temp_chars = chars.clone();
769 let mut brace_content = String::new();
770 let mut depth = 1;
771
772 temp_chars.next(); while let Some(&ch) = temp_chars.peek() {
775 if ch == '{' {
776 depth += 1;
777 } else if ch == '}' {
778 depth -= 1;
779 if depth == 0 {
780 break;
781 }
782 }
783 brace_content.push(ch);
784 temp_chars.next();
785 }
786
787 if depth == 0 && !brace_content.trim().is_empty() {
788 if brace_content.contains(',') || brace_content.contains("..") {
791 current.push('{');
793 current.push_str(&brace_content);
794 current.push('}');
795 chars.next(); let mut content_depth = 1;
798 while let Some(&ch) = chars.peek() {
799 chars.next();
800 if ch == '{' {
801 content_depth += 1;
802 } else if ch == '}' {
803 content_depth -= 1;
804 if content_depth == 0 {
805 break;
806 }
807 }
808 }
809 } else {
810 if !current.is_empty() {
812 if let Some(keyword) = is_keyword(¤t) {
813 tokens.push(keyword);
814 } else {
815 tokens.push(Token::Word(current.clone()));
816 }
817 current.clear();
818 }
819 tokens.push(Token::LeftBrace);
820 chars.next();
821 }
822 } else {
823 if !current.is_empty() {
825 if let Some(keyword) = is_keyword(¤t) {
826 tokens.push(keyword);
827 } else {
828 tokens.push(Token::Word(current.clone()));
829 }
830 current.clear();
831 }
832 tokens.push(Token::LeftBrace);
833 chars.next();
834 }
835 }
836 '`' => {
837 if !current.is_empty() {
838 if let Some(keyword) = is_keyword(¤t) {
839 tokens.push(keyword);
840 } else {
841 tokens.push(Token::Word(current.clone()));
843 }
844 current.clear();
845 }
846 chars.next();
847 let mut sub_command = String::new();
848 while let Some(&ch) = chars.peek() {
849 if ch == '`' {
850 chars.next();
851 break;
852 } else {
853 sub_command.push(ch);
854 chars.next();
855 }
856 }
857 current.push('`');
859 current.push_str(&sub_command);
860 current.push('`');
861 }
862 ';' => {
863 if !current.is_empty() {
864 if let Some(keyword) = is_keyword(¤t) {
865 tokens.push(keyword);
866 } else {
867 tokens.push(Token::Word(current.clone()));
869 }
870 current.clear();
871 }
872 chars.next();
873 if let Some(&next_ch) = chars.peek() {
874 if next_ch == ';' {
875 chars.next();
876 tokens.push(Token::DoubleSemicolon);
877 } else {
878 tokens.push(Token::Semicolon);
879 }
880 } else {
881 tokens.push(Token::Semicolon);
882 }
883 }
884 _ => {
885 if ch == '~' && current.is_empty() {
886 if let Ok(home) = env::var("HOME") {
887 current.push_str(&home);
888 } else {
889 current.push('~');
890 }
891 } else {
892 current.push(ch);
893 }
894 chars.next();
895 }
896 }
897 }
898 if !current.is_empty() {
899 if let Some(keyword) = is_keyword(¤t) {
900 tokens.push(keyword);
901 } else {
902 tokens.push(Token::Word(current.clone()));
904 }
905 }
906
907 Ok(tokens)
908}
909
910pub fn expand_aliases(
912 tokens: Vec<Token>,
913 shell_state: &ShellState,
914 expanded: &mut HashSet<String>,
915) -> Result<Vec<Token>, String> {
916 if tokens.is_empty() {
917 return Ok(tokens);
918 }
919
920 if let Token::Word(ref word) = tokens[0] {
922 if let Some(alias_value) = shell_state.get_alias(word) {
923 if expanded.contains(word) {
925 return Err(format!("Alias '{}' recursion detected", word));
926 }
927
928 expanded.insert(word.clone());
930
931 let alias_tokens = lex(alias_value, shell_state)?;
933
934 let expanded_alias_tokens = if !alias_tokens.is_empty() {
942 if let Token::Word(ref first_word) = alias_tokens[0] {
943 if first_word != word && shell_state.get_alias(first_word).is_some() && !expanded.contains(first_word) {
945 expand_aliases(alias_tokens, shell_state, expanded)?
946 } else {
947 alias_tokens
948 }
949 } else {
950 alias_tokens
951 }
952 } else {
953 alias_tokens
954 };
955
956 expanded.remove(word);
958
959 let mut result = expanded_alias_tokens;
961 result.extend_from_slice(&tokens[1..]);
962 Ok(result)
963 } else {
964 Ok(tokens)
966 }
967 } else {
968 Ok(tokens)
970 }
971}
972
973#[cfg(test)]
974mod tests {
975 use super::*;
976
977 fn expand_tokens(tokens: Vec<Token>, shell_state: &mut crate::state::ShellState) -> Vec<Token> {
980 let mut result = Vec::new();
981 for token in tokens {
982 match token {
983 Token::Word(word) => {
984 let expanded = crate::executor::expand_variables_in_string(&word, shell_state);
986 if !expanded.is_empty() || !word.starts_with("$(") {
989 result.push(Token::Word(expanded));
990 }
991 }
992 other => result.push(other),
993 }
994 }
995 result
996 }
997
998 #[test]
999 fn test_basic_word() {
1000 let shell_state = crate::state::ShellState::new();
1001 let result = lex("ls", &shell_state).unwrap();
1002 assert_eq!(result, vec![Token::Word("ls".to_string())]);
1003 }
1004
1005 #[test]
1006 fn test_multiple_words() {
1007 let shell_state = crate::state::ShellState::new();
1008 let result = lex("ls -la", &shell_state).unwrap();
1009 assert_eq!(
1010 result,
1011 vec![
1012 Token::Word("ls".to_string()),
1013 Token::Word("-la".to_string())
1014 ]
1015 );
1016 }
1017
1018 #[test]
1019 fn test_pipe() {
1020 let shell_state = crate::state::ShellState::new();
1021 let result = lex("ls | grep txt", &shell_state).unwrap();
1022 assert_eq!(
1023 result,
1024 vec![
1025 Token::Word("ls".to_string()),
1026 Token::Pipe,
1027 Token::Word("grep".to_string()),
1028 Token::Word("txt".to_string())
1029 ]
1030 );
1031 }
1032
1033 #[test]
1034 fn test_redirections() {
1035 let shell_state = crate::state::ShellState::new();
1036 let result = lex("printf hello > output.txt", &shell_state).unwrap();
1037 assert_eq!(
1038 result,
1039 vec![
1040 Token::Word("printf".to_string()),
1041 Token::Word("hello".to_string()),
1042 Token::RedirOut,
1043 Token::Word("output.txt".to_string())
1044 ]
1045 );
1046 }
1047
1048 #[test]
1049 fn test_append_redirection() {
1050 let shell_state = crate::state::ShellState::new();
1051 let result = lex("printf hello >> output.txt", &shell_state).unwrap();
1052 assert_eq!(
1053 result,
1054 vec![
1055 Token::Word("printf".to_string()),
1056 Token::Word("hello".to_string()),
1057 Token::RedirAppend,
1058 Token::Word("output.txt".to_string())
1059 ]
1060 );
1061 }
1062
1063 #[test]
1064 fn test_input_redirection() {
1065 let shell_state = crate::state::ShellState::new();
1066 let result = lex("cat < input.txt", &shell_state).unwrap();
1067 assert_eq!(
1068 result,
1069 vec![
1070 Token::Word("cat".to_string()),
1071 Token::RedirIn,
1072 Token::Word("input.txt".to_string())
1073 ]
1074 );
1075 }
1076
1077 #[test]
1078 fn test_double_quotes() {
1079 let shell_state = crate::state::ShellState::new();
1080 let result = lex("echo \"hello world\"", &shell_state).unwrap();
1081 assert_eq!(
1082 result,
1083 vec![
1084 Token::Word("echo".to_string()),
1085 Token::Word("hello world".to_string())
1086 ]
1087 );
1088 }
1089
1090 #[test]
1091 fn test_single_quotes() {
1092 let shell_state = crate::state::ShellState::new();
1093 let result = lex("echo 'hello world'", &shell_state).unwrap();
1094 assert_eq!(
1095 result,
1096 vec![
1097 Token::Word("echo".to_string()),
1098 Token::Word("hello world".to_string())
1099 ]
1100 );
1101 }
1102
1103 #[test]
1104 fn test_variable_expansion() {
1105 let mut shell_state = crate::state::ShellState::new();
1106 shell_state.set_var("TEST_VAR", "expanded_value".to_string());
1107 let tokens = lex("echo $TEST_VAR", &shell_state).unwrap();
1108 let result = expand_tokens(tokens, &mut shell_state);
1109 assert_eq!(
1110 result,
1111 vec![
1112 Token::Word("echo".to_string()),
1113 Token::Word("expanded_value".to_string())
1114 ]
1115 );
1116 }
1117
1118 #[test]
1119 fn test_variable_expansion_nonexistent() {
1120 let shell_state = crate::state::ShellState::new();
1121 let result = lex("echo $TEST_VAR2", &shell_state).unwrap();
1122 assert_eq!(
1123 result,
1124 vec![
1125 Token::Word("echo".to_string()),
1126 Token::Word("$TEST_VAR2".to_string())
1127 ]
1128 );
1129 }
1130
1131 #[test]
1132 fn test_empty_variable() {
1133 let shell_state = crate::state::ShellState::new();
1134 let result = lex("echo $", &shell_state).unwrap();
1135 assert_eq!(
1136 result,
1137 vec![
1138 Token::Word("echo".to_string()),
1139 Token::Word("$".to_string())
1140 ]
1141 );
1142 }
1143
1144 #[test]
1145 fn test_mixed_quotes_and_variables() {
1146 let mut shell_state = crate::state::ShellState::new();
1147 shell_state.set_var("USER", "alice".to_string());
1148 let tokens = lex("echo \"Hello $USER\"", &shell_state).unwrap();
1149 let result = expand_tokens(tokens, &mut shell_state);
1150 assert_eq!(
1151 result,
1152 vec![
1153 Token::Word("echo".to_string()),
1154 Token::Word("Hello alice".to_string())
1155 ]
1156 );
1157 }
1158
1159 #[test]
1160 fn test_unclosed_double_quote() {
1161 let shell_state = crate::state::ShellState::new();
1163 let result = lex("echo \"hello", &shell_state).unwrap();
1164 assert_eq!(
1165 result,
1166 vec![
1167 Token::Word("echo".to_string()),
1168 Token::Word("hello".to_string())
1169 ]
1170 );
1171 }
1172
1173 #[test]
1174 fn test_empty_input() {
1175 let shell_state = crate::state::ShellState::new();
1176 let result = lex("", &shell_state).unwrap();
1177 assert_eq!(result, Vec::<Token>::new());
1178 }
1179
1180 #[test]
1181 fn test_only_spaces() {
1182 let shell_state = crate::state::ShellState::new();
1183 let result = lex(" ", &shell_state).unwrap();
1184 assert_eq!(result, Vec::<Token>::new());
1185 }
1186
1187 #[test]
1188 fn test_complex_pipeline() {
1189 let shell_state = crate::state::ShellState::new();
1190 let result = lex(
1191 "cat input.txt | grep \"search term\" > output.txt",
1192 &shell_state,
1193 )
1194 .unwrap();
1195 assert_eq!(
1196 result,
1197 vec![
1198 Token::Word("cat".to_string()),
1199 Token::Word("input.txt".to_string()),
1200 Token::Pipe,
1201 Token::Word("grep".to_string()),
1202 Token::Word("search term".to_string()),
1203 Token::RedirOut,
1204 Token::Word("output.txt".to_string())
1205 ]
1206 );
1207 }
1208
1209 #[test]
1210 fn test_if_tokens() {
1211 let shell_state = crate::state::ShellState::new();
1212 let result = lex("if true; then printf yes; fi", &shell_state).unwrap();
1213 assert_eq!(
1214 result,
1215 vec![
1216 Token::If,
1217 Token::Word("true".to_string()),
1218 Token::Semicolon,
1219 Token::Then,
1220 Token::Word("printf".to_string()),
1221 Token::Word("yes".to_string()),
1222 Token::Semicolon,
1223 Token::Fi,
1224 ]
1225 );
1226 }
1227
1228 #[test]
1229 fn test_command_substitution_dollar_paren() {
1230 let shell_state = crate::state::ShellState::new();
1231 let result = lex("echo $(pwd)", &shell_state).unwrap();
1232 assert_eq!(result.len(), 2);
1234 assert_eq!(result[0], Token::Word("echo".to_string()));
1235 assert!(matches!(result[1], Token::Word(_)));
1236 }
1237
1238 #[test]
1239 fn test_command_substitution_backticks() {
1240 let shell_state = crate::state::ShellState::new();
1241 let result = lex("echo `pwd`", &shell_state).unwrap();
1242 assert_eq!(result.len(), 2);
1244 assert_eq!(result[0], Token::Word("echo".to_string()));
1245 assert!(matches!(result[1], Token::Word(_)));
1246 }
1247
1248 #[test]
1249 fn test_command_substitution_with_arguments() {
1250 let mut shell_state = crate::state::ShellState::new();
1251 let tokens = lex("echo $(echo hello world)", &shell_state).unwrap();
1252 let result = expand_tokens(tokens, &mut shell_state);
1253 assert_eq!(
1254 result,
1255 vec![
1256 Token::Word("echo".to_string()),
1257 Token::Word("hello world".to_string())
1258 ]
1259 );
1260 }
1261
1262 #[test]
1263 fn test_command_substitution_backticks_with_arguments() {
1264 let mut shell_state = crate::state::ShellState::new();
1265 let tokens = lex("echo `echo hello world`", &shell_state).unwrap();
1266 let result = expand_tokens(tokens, &mut shell_state);
1267 assert_eq!(
1268 result,
1269 vec![
1270 Token::Word("echo".to_string()),
1271 Token::Word("hello world".to_string())
1272 ]
1273 );
1274 }
1275
1276 #[test]
1277 fn test_command_substitution_failure_fallback() {
1278 let shell_state = crate::state::ShellState::new();
1279 let result = lex("echo $(nonexistent_command)", &shell_state).unwrap();
1280 assert_eq!(
1281 result,
1282 vec![
1283 Token::Word("echo".to_string()),
1284 Token::Word("$(nonexistent_command)".to_string())
1285 ]
1286 );
1287 }
1288
1289 #[test]
1290 fn test_command_substitution_backticks_failure_fallback() {
1291 let shell_state = crate::state::ShellState::new();
1292 let result = lex("echo `nonexistent_command`", &shell_state).unwrap();
1293 assert_eq!(
1294 result,
1295 vec![
1296 Token::Word("echo".to_string()),
1297 Token::Word("`nonexistent_command`".to_string())
1298 ]
1299 );
1300 }
1301
1302 #[test]
1303 fn test_command_substitution_with_variables() {
1304 let mut shell_state = crate::state::ShellState::new();
1305 shell_state.set_var("TEST_VAR", "test_value".to_string());
1306 let tokens = lex("echo $(echo $TEST_VAR)", &shell_state).unwrap();
1307 let result = expand_tokens(tokens, &mut shell_state);
1308 assert_eq!(
1309 result,
1310 vec![
1311 Token::Word("echo".to_string()),
1312 Token::Word("test_value".to_string())
1313 ]
1314 );
1315 }
1316
1317 #[test]
1318 fn test_command_substitution_in_assignment() {
1319 let mut shell_state = crate::state::ShellState::new();
1320 let tokens = lex("MY_VAR=$(echo hello)", &shell_state).unwrap();
1321 let result = expand_tokens(tokens, &mut shell_state);
1322 assert_eq!(result, vec![Token::Word("MY_VAR=hello".to_string())]);
1324 }
1325
1326 #[test]
1327 fn test_command_substitution_backticks_in_assignment() {
1328 let mut shell_state = crate::state::ShellState::new();
1329 let tokens = lex("MY_VAR=`echo hello`", &shell_state).unwrap();
1330 let result = expand_tokens(tokens, &mut shell_state);
1331 assert_eq!(
1333 result,
1334 vec![
1335 Token::Word("MY_VAR=".to_string()),
1336 Token::Word("hello".to_string())
1337 ]
1338 );
1339 }
1340
1341 #[test]
1342 fn test_command_substitution_with_quotes() {
1343 let mut shell_state = crate::state::ShellState::new();
1344 let tokens = lex("echo \"$(echo hello world)\"", &shell_state).unwrap();
1345 let result = expand_tokens(tokens, &mut shell_state);
1346 assert_eq!(
1347 result,
1348 vec![
1349 Token::Word("echo".to_string()),
1350 Token::Word("hello world".to_string())
1351 ]
1352 );
1353 }
1354
1355 #[test]
1356 fn test_command_substitution_backticks_with_quotes() {
1357 let mut shell_state = crate::state::ShellState::new();
1358 let tokens = lex("echo \"`echo hello world`\"", &shell_state).unwrap();
1359 let result = expand_tokens(tokens, &mut shell_state);
1360 assert_eq!(
1361 result,
1362 vec![
1363 Token::Word("echo".to_string()),
1364 Token::Word("hello world".to_string())
1365 ]
1366 );
1367 }
1368
1369 #[test]
1370 fn test_command_substitution_empty_output() {
1371 let mut shell_state = crate::state::ShellState::new();
1372 let tokens = lex("echo $(true)", &shell_state).unwrap();
1373 let result = expand_tokens(tokens, &mut shell_state);
1374 assert_eq!(result, vec![Token::Word("echo".to_string())]);
1376 }
1377
1378 #[test]
1379 fn test_command_substitution_multiple_spaces() {
1380 let mut shell_state = crate::state::ShellState::new();
1381 let tokens = lex("echo $(echo 'hello world')", &shell_state).unwrap();
1382 let result = expand_tokens(tokens, &mut shell_state);
1383 assert_eq!(
1384 result,
1385 vec![
1386 Token::Word("echo".to_string()),
1387 Token::Word("hello world".to_string())
1388 ]
1389 );
1390 }
1391
1392 #[test]
1393 fn test_command_substitution_with_newlines() {
1394 let mut shell_state = crate::state::ShellState::new();
1395 let tokens = lex("echo $(printf 'hello\nworld')", &shell_state).unwrap();
1396 let result = expand_tokens(tokens, &mut shell_state);
1397 assert_eq!(
1398 result,
1399 vec![
1400 Token::Word("echo".to_string()),
1401 Token::Word("hello\nworld".to_string())
1402 ]
1403 );
1404 }
1405
1406 #[test]
1407 fn test_command_substitution_special_characters() {
1408 let shell_state = crate::state::ShellState::new();
1409 let result = lex("echo $(echo '$#@^&*()')", &shell_state).unwrap();
1410 println!("Special chars test result: {:?}", result);
1411 assert_eq!(result.len(), 2);
1414 assert_eq!(result[0], Token::Word("echo".to_string()));
1415 assert!(matches!(result[1], Token::Word(_)));
1416 }
1417
1418 #[test]
1419 fn test_nested_command_substitution() {
1420 let shell_state = crate::state::ShellState::new();
1423 let result = lex("echo $(echo $(pwd))", &shell_state).unwrap();
1424 assert_eq!(result.len(), 2);
1426 assert_eq!(result[0], Token::Word("echo".to_string()));
1427 assert!(matches!(result[1], Token::Word(_)));
1428 }
1429
1430 #[test]
1431 fn test_command_substitution_in_pipeline() {
1432 let shell_state = crate::state::ShellState::new();
1433 let result = lex("$(echo hello) | cat", &shell_state).unwrap();
1434 println!("Pipeline test result: {:?}", result);
1435 assert_eq!(result.len(), 3);
1436 assert!(matches!(result[0], Token::Word(_)));
1437 assert_eq!(result[1], Token::Pipe);
1438 assert_eq!(result[2], Token::Word("cat".to_string()));
1439 }
1440
1441 #[test]
1442 fn test_command_substitution_with_redirection() {
1443 let shell_state = crate::state::ShellState::new();
1444 let result = lex("$(echo hello) > output.txt", &shell_state).unwrap();
1445 assert_eq!(result.len(), 3);
1446 assert!(matches!(result[0], Token::Word(_)));
1447 assert_eq!(result[1], Token::RedirOut);
1448 assert_eq!(result[2], Token::Word("output.txt".to_string()));
1449 }
1450
1451 #[test]
1452 fn test_variable_in_quotes_with_pipe() {
1453 let mut shell_state = crate::state::ShellState::new();
1454 shell_state.set_var("PATH", "/usr/bin:/bin".to_string());
1455 let tokens = lex("echo \"$PATH\" | tr ':' '\\n'", &shell_state).unwrap();
1456 let result = expand_tokens(tokens, &mut shell_state);
1457 assert_eq!(
1458 result,
1459 vec![
1460 Token::Word("echo".to_string()),
1461 Token::Word("/usr/bin:/bin".to_string()),
1462 Token::Pipe,
1463 Token::Word("tr".to_string()),
1464 Token::Word(":".to_string()),
1465 Token::Word("\\n".to_string())
1466 ]
1467 );
1468 }
1469
1470 #[test]
1471 fn test_expand_aliases_simple() {
1472 let mut shell_state = crate::state::ShellState::new();
1473 shell_state.set_alias("ll", "ls -l".to_string());
1474 let tokens = vec![Token::Word("ll".to_string())];
1475 let result =
1476 expand_aliases(tokens, &shell_state, &mut std::collections::HashSet::new()).unwrap();
1477 assert_eq!(
1478 result,
1479 vec![Token::Word("ls".to_string()), Token::Word("-l".to_string())]
1480 );
1481 }
1482
1483 #[test]
1484 fn test_expand_aliases_with_args() {
1485 let mut shell_state = crate::state::ShellState::new();
1486 shell_state.set_alias("ll", "ls -l".to_string());
1487 let tokens = vec![
1488 Token::Word("ll".to_string()),
1489 Token::Word("/tmp".to_string()),
1490 ];
1491 let result =
1492 expand_aliases(tokens, &shell_state, &mut std::collections::HashSet::new()).unwrap();
1493 assert_eq!(
1494 result,
1495 vec![
1496 Token::Word("ls".to_string()),
1497 Token::Word("-l".to_string()),
1498 Token::Word("/tmp".to_string())
1499 ]
1500 );
1501 }
1502
1503 #[test]
1504 fn test_expand_aliases_no_alias() {
1505 let shell_state = crate::state::ShellState::new();
1506 let tokens = vec![Token::Word("ls".to_string())];
1507 let result = expand_aliases(
1508 tokens.clone(),
1509 &shell_state,
1510 &mut std::collections::HashSet::new(),
1511 )
1512 .unwrap();
1513 assert_eq!(result, tokens);
1514 }
1515
1516 #[test]
1517 fn test_expand_aliases_chained() {
1518 let mut shell_state = crate::state::ShellState::new();
1522 shell_state.set_alias("a", "b".to_string());
1523 shell_state.set_alias("b", "a".to_string());
1524 let tokens = vec![Token::Word("a".to_string())];
1525 let result = expand_aliases(tokens, &shell_state, &mut std::collections::HashSet::new());
1526 assert!(result.is_ok());
1528 assert_eq!(result.unwrap(), vec![Token::Word("a".to_string())]);
1529 }
1530
1531 #[test]
1532 fn test_arithmetic_expansion_simple() {
1533 let mut shell_state = crate::state::ShellState::new();
1534 let tokens = lex("echo $((2 + 3))", &shell_state).unwrap();
1535 let result = expand_tokens(tokens, &mut shell_state);
1536 assert_eq!(
1537 result,
1538 vec![
1539 Token::Word("echo".to_string()),
1540 Token::Word("5".to_string())
1541 ]
1542 );
1543 }
1544
1545 #[test]
1546 fn test_arithmetic_expansion_with_variables() {
1547 let mut shell_state = crate::state::ShellState::new();
1548 shell_state.set_var("x", "10".to_string());
1549 shell_state.set_var("y", "20".to_string());
1550 let tokens = lex("echo $((x + y * 2))", &shell_state).unwrap();
1551 let result = expand_tokens(tokens, &mut shell_state);
1552 assert_eq!(
1553 result,
1554 vec![
1555 Token::Word("echo".to_string()),
1556 Token::Word("50".to_string()) ]
1558 );
1559 }
1560
1561 #[test]
1562 fn test_arithmetic_expansion_comparison() {
1563 let mut shell_state = crate::state::ShellState::new();
1564 let tokens = lex("echo $((5 > 3))", &shell_state).unwrap();
1565 let result = expand_tokens(tokens, &mut shell_state);
1566 assert_eq!(
1567 result,
1568 vec![
1569 Token::Word("echo".to_string()),
1570 Token::Word("1".to_string()) ]
1572 );
1573 }
1574
1575 #[test]
1576 fn test_arithmetic_expansion_complex() {
1577 let mut shell_state = crate::state::ShellState::new();
1578 shell_state.set_var("a", "3".to_string());
1579 let tokens = lex("echo $((a * 2 + 5))", &shell_state).unwrap();
1580 let result = expand_tokens(tokens, &mut shell_state);
1581 assert_eq!(
1582 result,
1583 vec![
1584 Token::Word("echo".to_string()),
1585 Token::Word("11".to_string()) ]
1587 );
1588 }
1589
1590 #[test]
1591 fn test_arithmetic_expansion_unmatched_parentheses() {
1592 let mut shell_state = crate::state::ShellState::new();
1593 let tokens = lex("echo $((2 + 3", &shell_state).unwrap();
1594 let result = expand_tokens(tokens, &mut shell_state);
1595 assert_eq!(result.len(), 2);
1597 assert_eq!(result[0], Token::Word("echo".to_string()));
1598 let second_token = &result[1];
1600 if let Token::Word(s) = second_token {
1601 assert!(s.starts_with("$((") && s.contains("2") && s.contains("3"),
1602 "Expected unmatched arithmetic to be kept as literal, got: {}", s);
1603 } else {
1604 panic!("Expected Word token");
1605 }
1606 }
1607
1608 #[test]
1609 fn test_arithmetic_expansion_division_by_zero() {
1610 let mut shell_state = crate::state::ShellState::new();
1611 let tokens = lex("echo $((5 / 0))", &shell_state).unwrap();
1612 let result = expand_tokens(tokens, &mut shell_state);
1613 assert_eq!(result.len(), 2);
1615 assert_eq!(result[0], Token::Word("echo".to_string()));
1616 if let Token::Word(s) = &result[1] {
1618 assert!(s.contains("Division by zero"), "Expected division by zero error, got: {}", s);
1619 } else {
1620 panic!("Expected Word token");
1621 }
1622 }
1623
1624 #[test]
1625 fn test_parameter_expansion_simple() {
1626 let mut shell_state = crate::state::ShellState::new();
1627 shell_state.set_var("TEST_VAR", "hello world".to_string());
1628 let result = lex("echo ${TEST_VAR}", &shell_state).unwrap();
1629 assert_eq!(
1630 result,
1631 vec![
1632 Token::Word("echo".to_string()),
1633 Token::Word("hello world".to_string())
1634 ]
1635 );
1636 }
1637
1638 #[test]
1639 fn test_parameter_expansion_unset_variable() {
1640 let shell_state = crate::state::ShellState::new();
1641 let result = lex("echo ${UNSET_VAR}", &shell_state).unwrap();
1642 assert_eq!(
1643 result,
1644 vec![Token::Word("echo".to_string()), Token::Word("".to_string())]
1645 );
1646 }
1647
1648 #[test]
1649 fn test_parameter_expansion_default() {
1650 let shell_state = crate::state::ShellState::new();
1651 let result = lex("echo ${UNSET_VAR:-default}", &shell_state).unwrap();
1652 assert_eq!(
1653 result,
1654 vec![
1655 Token::Word("echo".to_string()),
1656 Token::Word("default".to_string())
1657 ]
1658 );
1659 }
1660
1661 #[test]
1662 fn test_parameter_expansion_default_set_variable() {
1663 let mut shell_state = crate::state::ShellState::new();
1664 shell_state.set_var("TEST_VAR", "value".to_string());
1665 let result = lex("echo ${TEST_VAR:-default}", &shell_state).unwrap();
1666 assert_eq!(
1667 result,
1668 vec![
1669 Token::Word("echo".to_string()),
1670 Token::Word("value".to_string())
1671 ]
1672 );
1673 }
1674
1675 #[test]
1676 fn test_parameter_expansion_assign_default() {
1677 let shell_state = crate::state::ShellState::new();
1678 let result = lex("echo ${UNSET_VAR:=default}", &shell_state).unwrap();
1679 assert_eq!(
1680 result,
1681 vec![
1682 Token::Word("echo".to_string()),
1683 Token::Word("default".to_string())
1684 ]
1685 );
1686 }
1687
1688 #[test]
1689 fn test_parameter_expansion_alternative() {
1690 let mut shell_state = crate::state::ShellState::new();
1691 shell_state.set_var("TEST_VAR", "value".to_string());
1692 let result = lex("echo ${TEST_VAR:+replacement}", &shell_state).unwrap();
1693 assert_eq!(
1694 result,
1695 vec![
1696 Token::Word("echo".to_string()),
1697 Token::Word("replacement".to_string())
1698 ]
1699 );
1700 }
1701
1702 #[test]
1703 fn test_parameter_expansion_alternative_unset() {
1704 let shell_state = crate::state::ShellState::new();
1705 let result = lex("echo ${UNSET_VAR:+replacement}", &shell_state).unwrap();
1706 assert_eq!(
1707 result,
1708 vec![Token::Word("echo".to_string()), Token::Word("".to_string())]
1709 );
1710 }
1711
1712 #[test]
1713 fn test_parameter_expansion_substring() {
1714 let mut shell_state = crate::state::ShellState::new();
1715 shell_state.set_var("TEST_VAR", "hello world".to_string());
1716 let result = lex("echo ${TEST_VAR:6}", &shell_state).unwrap();
1717 assert_eq!(
1718 result,
1719 vec![
1720 Token::Word("echo".to_string()),
1721 Token::Word("world".to_string())
1722 ]
1723 );
1724 }
1725
1726 #[test]
1727 fn test_parameter_expansion_substring_with_length() {
1728 let mut shell_state = crate::state::ShellState::new();
1729 shell_state.set_var("TEST_VAR", "hello world".to_string());
1730 let result = lex("echo ${TEST_VAR:0:5}", &shell_state).unwrap();
1731 assert_eq!(
1732 result,
1733 vec![
1734 Token::Word("echo".to_string()),
1735 Token::Word("hello".to_string())
1736 ]
1737 );
1738 }
1739
1740 #[test]
1741 fn test_parameter_expansion_length() {
1742 let mut shell_state = crate::state::ShellState::new();
1743 shell_state.set_var("TEST_VAR", "hello".to_string());
1744 let result = lex("echo ${#TEST_VAR}", &shell_state).unwrap();
1745 assert_eq!(
1746 result,
1747 vec![
1748 Token::Word("echo".to_string()),
1749 Token::Word("5".to_string())
1750 ]
1751 );
1752 }
1753
1754 #[test]
1755 fn test_parameter_expansion_remove_shortest_prefix() {
1756 let mut shell_state = crate::state::ShellState::new();
1757 shell_state.set_var("TEST_VAR", "prefix_hello".to_string());
1758 let result = lex("echo ${TEST_VAR#prefix_}", &shell_state).unwrap();
1759 assert_eq!(
1760 result,
1761 vec![
1762 Token::Word("echo".to_string()),
1763 Token::Word("hello".to_string())
1764 ]
1765 );
1766 }
1767
1768 #[test]
1769 fn test_parameter_expansion_remove_longest_prefix() {
1770 let mut shell_state = crate::state::ShellState::new();
1771 shell_state.set_var("TEST_VAR", "prefix_prefix_hello".to_string());
1772 let result = lex("echo ${TEST_VAR##prefix_}", &shell_state).unwrap();
1773 assert_eq!(
1774 result,
1775 vec![
1776 Token::Word("echo".to_string()),
1777 Token::Word("prefix_hello".to_string())
1778 ]
1779 );
1780 }
1781
1782 #[test]
1783 fn test_parameter_expansion_remove_shortest_suffix() {
1784 let mut shell_state = crate::state::ShellState::new();
1785 shell_state.set_var("TEST_VAR", "hello_suffix".to_string());
1786 let result = lex("echo ${TEST_VAR%suffix}", &shell_state).unwrap();
1787 assert_eq!(
1788 result,
1789 vec![
1790 Token::Word("echo".to_string()),
1791 Token::Word("hello_".to_string()) ]
1793 );
1794 }
1795
1796 #[test]
1797 fn test_parameter_expansion_remove_longest_suffix() {
1798 let mut shell_state = crate::state::ShellState::new();
1799 shell_state.set_var("TEST_VAR", "hello_suffix_suffix".to_string());
1800 let result = lex("echo ${TEST_VAR%%suffix}", &shell_state).unwrap();
1801 assert_eq!(
1802 result,
1803 vec![
1804 Token::Word("echo".to_string()),
1805 Token::Word("hello_suffix_".to_string()) ]
1807 );
1808 }
1809
1810 #[test]
1811 fn test_parameter_expansion_substitute() {
1812 let mut shell_state = crate::state::ShellState::new();
1813 shell_state.set_var("TEST_VAR", "hello world".to_string());
1814 let result = lex("echo ${TEST_VAR/world/universe}", &shell_state).unwrap();
1815 assert_eq!(
1816 result,
1817 vec![
1818 Token::Word("echo".to_string()),
1819 Token::Word("hello universe".to_string())
1820 ]
1821 );
1822 }
1823
1824 #[test]
1825 fn test_parameter_expansion_substitute_all() {
1826 let mut shell_state = crate::state::ShellState::new();
1827 shell_state.set_var("TEST_VAR", "hello world world".to_string());
1828 let result = lex("echo ${TEST_VAR//world/universe}", &shell_state).unwrap();
1829 assert_eq!(
1830 result,
1831 vec![
1832 Token::Word("echo".to_string()),
1833 Token::Word("hello universe universe".to_string())
1834 ]
1835 );
1836 }
1837
1838 #[test]
1839 fn test_parameter_expansion_mixed_with_regular_variables() {
1840 let mut shell_state = crate::state::ShellState::new();
1841 shell_state.set_var("VAR1", "value1".to_string());
1842 shell_state.set_var("VAR2", "value2".to_string());
1843 let tokens = lex("echo $VAR1 and ${VAR2}", &shell_state).unwrap();
1844 let result = expand_tokens(tokens, &mut shell_state);
1845 assert_eq!(
1846 result,
1847 vec![
1848 Token::Word("echo".to_string()),
1849 Token::Word("value1".to_string()),
1850 Token::Word("and".to_string()),
1851 Token::Word("value2".to_string())
1852 ]
1853 );
1854 }
1855
1856 #[test]
1857 fn test_parameter_expansion_in_double_quotes() {
1858 let mut shell_state = crate::state::ShellState::new();
1859 shell_state.set_var("TEST_VAR", "hello".to_string());
1860 let result = lex("echo \"Value: ${TEST_VAR}\"", &shell_state).unwrap();
1861 assert_eq!(
1862 result,
1863 vec![
1864 Token::Word("echo".to_string()),
1865 Token::Word("Value: hello".to_string())
1866 ]
1867 );
1868 }
1869
1870 #[test]
1871 fn test_parameter_expansion_error_unset() {
1872 let shell_state = crate::state::ShellState::new();
1873 let result = lex("echo ${UNSET_VAR:?error message}", &shell_state);
1874 assert!(result.is_ok());
1876 let tokens = result.unwrap();
1877 assert_eq!(tokens.len(), 3);
1878 assert_eq!(tokens[0], Token::Word("echo".to_string()));
1879 assert_eq!(tokens[1], Token::Word("${UNSET_VAR:?error}".to_string()));
1880 assert_eq!(tokens[2], Token::Word("message}".to_string()));
1881 }
1882
1883 #[test]
1884 fn test_parameter_expansion_complex_expression() {
1885 let mut shell_state = crate::state::ShellState::new();
1886 shell_state.set_var("PATH", "/usr/bin:/bin:/usr/local/bin".to_string());
1887 let result = lex("echo ${PATH#/usr/bin:}", &shell_state).unwrap();
1888 assert_eq!(
1889 result,
1890 vec![
1891 Token::Word("echo".to_string()),
1892 Token::Word("/bin:/usr/local/bin".to_string())
1893 ]
1894 );
1895 }
1896
1897 #[test]
1898 fn test_local_keyword() {
1899 let shell_state = crate::state::ShellState::new();
1900 let result = lex("local myvar", &shell_state).unwrap();
1901 assert_eq!(
1902 result,
1903 vec![
1904 Token::Local,
1905 Token::Word("myvar".to_string())
1906 ]
1907 );
1908 }
1909
1910 #[test]
1911 fn test_local_keyword_in_function() {
1912 let shell_state = crate::state::ShellState::new();
1913 let result = lex("local var=value", &shell_state).unwrap();
1914 assert_eq!(
1915 result,
1916 vec![
1917 Token::Local,
1918 Token::Word("var=value".to_string())
1919 ]
1920 );
1921 }
1922}