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