1use std::collections::HashSet;
2use std::env;
3
4use super::parameter_expansion::{expand_parameter, parse_parameter_expansion};
5use super::state::ShellState;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum Token {
9 Word(String),
10 Pipe,
11 RedirOut,
12 RedirIn,
13 RedirAppend,
14 RedirHereDoc(String, bool), RedirHereString(String), If,
17 Then,
18 Else,
19 Elif,
20 Fi,
21 Case,
22 In,
23 Esac,
24 DoubleSemicolon,
25 Semicolon,
26 RightParen,
27 LeftParen,
28 LeftBrace,
29 RightBrace,
30 Newline,
31 Local,
32 Return,
33 For,
34 Do,
35 Done,
36 While, And, Or, }
40
41fn is_keyword(word: &str) -> Option<Token> {
42 match word {
43 "if" => Some(Token::If),
44 "then" => Some(Token::Then),
45 "else" => Some(Token::Else),
46 "elif" => Some(Token::Elif),
47 "fi" => Some(Token::Fi),
48 "case" => Some(Token::Case),
49 "in" => Some(Token::In),
50 "esac" => Some(Token::Esac),
51 "local" => Some(Token::Local),
52 "return" => Some(Token::Return),
53 "for" => Some(Token::For),
54 "while" => Some(Token::While),
55 "do" => Some(Token::Do),
56 "done" => Some(Token::Done),
57 _ => None,
58 }
59}
60
61fn skip_whitespace(chars: &mut std::iter::Peekable<std::str::Chars>) {
63 while let Some(&ch) = chars.peek() {
64 if ch == ' ' || ch == '\t' {
65 chars.next();
66 } else {
67 break;
68 }
69 }
70}
71
72fn flush_current_token(current: &mut String, tokens: &mut Vec<Token>) {
74 if !current.is_empty() {
75 if let Some(keyword) = is_keyword(current) {
76 tokens.push(keyword);
77 } else {
78 tokens.push(Token::Word(current.clone()));
79 }
80 current.clear();
81 }
82}
83
84fn collect_until_closing_brace(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
87 let mut content = String::new();
88
89 while let Some(&ch) = chars.peek() {
90 if ch == '}' {
91 chars.next(); break;
93 } else {
94 content.push(ch);
95 chars.next();
96 }
97 }
98
99 content
100}
101
102fn collect_with_paren_depth(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
107 let mut content = String::new();
108 let mut paren_depth = 1; let mut in_single_quote = false;
110 let mut in_double_quote = false;
111
112 while let Some(&ch) = chars.peek() {
113 if ch == '\'' && !in_double_quote {
114 in_single_quote = !in_single_quote;
116 content.push(ch);
117 chars.next();
118 } else if ch == '"' && !in_single_quote {
119 in_double_quote = !in_double_quote;
121 content.push(ch);
122 chars.next();
123 } else if ch == '(' && !in_single_quote && !in_double_quote {
124 paren_depth += 1;
125 content.push(ch);
126 chars.next();
127 } else if ch == ')' && !in_single_quote && !in_double_quote {
128 paren_depth -= 1;
129 if paren_depth == 0 {
130 chars.next(); break;
132 } else {
133 content.push(ch);
134 chars.next();
135 }
136 } else {
137 content.push(ch);
138 chars.next();
139 }
140 }
141
142 content
143}
144
145fn parse_variable_name(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
150 let mut var_name = String::new();
151
152 if let Some(&ch) = chars.peek() {
154 if ch == '?'
155 || ch == '$'
156 || ch == '0'
157 || ch == '#'
158 || ch == '@'
159 || ch == '*'
160 || ch == '!'
161 || ch.is_ascii_digit()
162 {
163 var_name.push(ch);
164 chars.next();
165 } else {
166 while let Some(&ch) = chars.peek() {
169 if ch.is_alphanumeric() || ch == '_' {
170 var_name.push(ch);
171 chars.next();
172 } else {
173 break;
174 }
175 }
176 }
177 }
178
179 var_name
180}
181
182fn expand_variables_in_command(command: &str, shell_state: &ShellState) -> String {
183 if command.contains("$(") || command.contains('`') {
185 return command.to_string();
186 }
187
188 let mut chars = command.chars().peekable();
189 let mut current = String::new();
190
191 while let Some(&ch) = chars.peek() {
192 if ch == '$' {
193 chars.next(); if let Some(&'{') = chars.peek() {
195 chars.next(); let param_content = collect_until_closing_brace(&mut chars);
198
199 if !param_content.is_empty() {
200 if param_content.starts_with('#') && param_content.len() > 1 {
202 let var_name = ¶m_content[1..];
203 if let Some(val) = shell_state.get_var(var_name) {
204 current.push_str(&val.len().to_string());
205 } else {
206 current.push('0');
207 }
208 } else {
209 match parse_parameter_expansion(¶m_content) {
211 Ok(expansion) => {
212 match expand_parameter(&expansion, shell_state) {
213 Ok(expanded) => {
214 current.push_str(&expanded);
215 }
216 Err(_) => {
217 current.push_str("${");
219 current.push_str(¶m_content);
220 current.push('}');
221 }
222 }
223 }
224 Err(_) => {
225 current.push_str("${");
227 current.push_str(¶m_content);
228 current.push('}');
229 }
230 }
231 }
232 } else {
233 current.push_str("${}");
235 }
236 } else if let Some(&'(') = chars.peek() {
237 current.push('$');
239 current.push('(');
240 chars.next();
241 } else if let Some(&'`') = chars.peek() {
242 current.push('$');
244 current.push('`');
245 chars.next();
246 } else {
247 let var_name = parse_variable_name(&mut chars);
249
250 if !var_name.is_empty() {
251 if let Some(val) = shell_state.get_var(&var_name) {
252 current.push_str(&val);
253 } else {
254 current.push('$');
255 current.push_str(&var_name);
256 }
257 } else {
258 current.push('$');
259 }
260 }
261 } else if ch == '`' {
262 current.push(ch);
264 chars.next();
265 } else {
266 current.push(ch);
267 chars.next();
268 }
269 }
270
271 if current.contains('$') {
273 let mut final_result = String::new();
275 let mut chars = current.chars().peekable();
276
277 while let Some(&ch) = chars.peek() {
278 if ch == '$' {
279 chars.next(); if let Some(&'{') = chars.peek() {
281 chars.next(); let param_content = collect_until_closing_brace(&mut chars);
284
285 if !param_content.is_empty() {
286 if param_content.starts_with('#') && param_content.len() > 1 {
288 let var_name = ¶m_content[1..];
289 if let Some(val) = shell_state.get_var(var_name) {
290 final_result.push_str(&val.len().to_string());
291 } else {
292 final_result.push('0');
293 }
294 } else {
295 match parse_parameter_expansion(¶m_content) {
297 Ok(expansion) => {
298 match expand_parameter(&expansion, shell_state) {
299 Ok(expanded) => {
300 if expanded.is_empty() {
301 } else {
305 final_result.push_str(&expanded);
306 }
307 }
308 Err(_) => {
309 final_result.push_str("${");
311 final_result.push_str(¶m_content);
312 final_result.push('}');
313 }
314 }
315 }
316 Err(_) => {
317 final_result.push_str("${");
319 final_result.push_str(¶m_content);
320 final_result.push('}');
321 }
322 }
323 }
324 } else {
325 final_result.push_str("${}");
327 }
328 } else {
329 let var_name = parse_variable_name(&mut chars);
330
331 if !var_name.is_empty() {
332 if let Some(val) = shell_state.get_var(&var_name) {
333 final_result.push_str(&val);
334 } else {
335 final_result.push('$');
336 final_result.push_str(&var_name);
337 }
338 } else {
339 final_result.push('$');
340 }
341 }
342 } else {
343 final_result.push(ch);
344 chars.next();
345 }
346 }
347 final_result
348 } else {
349 current
350 }
351}
352
353pub fn lex(input: &str, shell_state: &ShellState) -> Result<Vec<Token>, String> {
354 let mut tokens = Vec::new();
355 let mut chars = input.chars().peekable();
356 let mut current = String::new();
357 let mut in_double_quote = false;
358 let mut in_single_quote = false;
359
360 while let Some(&ch) = chars.peek() {
361 match ch {
362 ' ' | '\t' if !in_double_quote && !in_single_quote => {
363 flush_current_token(&mut current, &mut tokens);
364 chars.next();
365 }
366 '\n' if !in_double_quote && !in_single_quote => {
367 flush_current_token(&mut current, &mut tokens);
368 tokens.push(Token::Newline);
369 chars.next();
370 }
371 '"' if !in_single_quote => {
372 let is_escaped = current.ends_with('\\');
374
375 if is_escaped && in_double_quote {
376 current.pop(); current.push('"'); chars.next(); } else {
381 chars.next(); if in_double_quote {
383 in_double_quote = false;
387 } else {
388 in_double_quote = true;
391 }
392 }
393 }
394 '\\' if in_double_quote => {
395 chars.next(); if let Some(&next_ch) = chars.peek() {
398 if next_ch == '$'
400 || next_ch == '`'
401 || next_ch == '"'
402 || next_ch == '\\'
403 || next_ch == '\n'
404 {
405 current.push(next_ch);
407 chars.next(); } else {
409 current.push('\\');
411 current.push(next_ch);
412 chars.next();
413 }
414 } else {
415 current.push('\\');
417 }
418 }
419 '\'' => {
420 if in_single_quote {
421 in_single_quote = false;
425 } else if !in_double_quote {
426 in_single_quote = true;
429 }
430 chars.next();
431 }
432 '$' if !in_single_quote => {
433 chars.next(); if let Some(&'{') = chars.peek() {
435 chars.next(); let param_content = collect_until_closing_brace(&mut chars);
438
439 if !param_content.is_empty() {
440 if param_content.starts_with('#') && param_content.len() > 1 {
442 let var_name = ¶m_content[1..];
443 if let Some(val) = shell_state.get_var(var_name) {
444 current.push_str(&val.len().to_string());
445 } else {
446 current.push('0');
447 }
448 } else {
449 match parse_parameter_expansion(¶m_content) {
451 Ok(expansion) => {
452 match expand_parameter(&expansion, shell_state) {
453 Ok(expanded) => {
454 if expanded.is_empty() {
455 if !in_double_quote && !in_single_quote {
458 if !current.is_empty() {
460 if let Some(keyword) = is_keyword(¤t)
461 {
462 tokens.push(keyword);
463 } else {
464 let word = expand_variables_in_command(
465 ¤t,
466 shell_state,
467 );
468 tokens.push(Token::Word(word));
469 }
470 current.clear();
471 }
472 tokens.push(Token::Word("".to_string()));
474 }
475 } else {
477 current.push_str(&expanded);
478 }
479 }
480 Err(_) => {
481 if !current.is_empty() {
483 if let Some(keyword) = is_keyword(¤t) {
484 tokens.push(keyword);
485 } else {
486 let word = expand_variables_in_command(
487 ¤t,
488 shell_state,
489 );
490 tokens.push(Token::Word(word));
491 }
492 current.clear();
493 }
494 if let Some(space_pos) = param_content.find(' ') {
496 let first_part =
498 format!("${{{}}}", ¶m_content[..space_pos]);
499 let second_part = format!(
500 "{}}}",
501 ¶m_content[space_pos + 1..]
502 );
503 tokens.push(Token::Word(first_part));
504 tokens.push(Token::Word(second_part));
505 } else {
506 let literal = format!("${{{}}}", param_content);
507 tokens.push(Token::Word(literal));
508 }
509 }
510 }
511 }
512 Err(_) => {
513 current.push_str("${");
515 current.push_str(¶m_content);
516 current.push('}');
517 }
518 }
519 }
520 } else {
521 current.push_str("${}");
523 }
524 } else if let Some(&'(') = chars.peek() {
525 chars.next(); if let Some(&'(') = chars.peek() {
527 chars.next(); let arithmetic_expr = collect_with_paren_depth(&mut chars);
530 let found_closing = if let Some(&')') = chars.peek() {
532 chars.next(); true
534 } else {
535 false
536 };
537 current.push_str("$((");
539 current.push_str(&arithmetic_expr);
540 if found_closing {
541 current.push_str("))");
542 }
543 } else {
544 let sub_command = collect_with_paren_depth(&mut chars);
547 current.push_str("$(");
549 current.push_str(&sub_command);
550 current.push(')');
551 }
552 } else {
553 let var_name = parse_variable_name(&mut chars);
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 flush_current_token(&mut current, &mut tokens);
567 chars.next(); if let Some(&'|') = chars.peek() {
570 chars.next(); tokens.push(Token::Or);
572 } else {
573 tokens.push(Token::Pipe);
574 }
575 skip_whitespace(&mut chars);
577 }
578 '&' if !in_double_quote && !in_single_quote => {
579 flush_current_token(&mut current, &mut tokens);
580 chars.next(); if let Some(&'&') = chars.peek() {
583 chars.next(); tokens.push(Token::And);
585 skip_whitespace(&mut chars);
587 } else {
588 current.push('&');
590 }
591 }
592 '>' if !in_double_quote && !in_single_quote => {
593 let is_fd_redirect = if !current.is_empty() {
596 current
597 .chars()
598 .last()
599 .map(|c| c.is_ascii_digit())
600 .unwrap_or(false)
601 } else {
602 false
603 };
604
605 if is_fd_redirect {
606 chars.next(); if let Some(&'&') = chars.peek() {
609 chars.next(); let mut target = String::new();
612 while let Some(&ch) = chars.peek() {
613 if ch.is_ascii_digit() || ch == '-' {
614 target.push(ch);
615 chars.next();
616 } else {
617 break;
618 }
619 }
620
621 if !target.is_empty() {
622 current.pop();
625
626 flush_current_token(&mut current, &mut tokens);
628
629 continue;
632 } else {
633 current.push('>');
635 current.push('&');
636 }
637 } else {
638 flush_current_token(&mut current, &mut tokens);
641
642 if let Some(&next_ch) = chars.peek() {
643 if next_ch == '>' {
644 chars.next();
645 tokens.push(Token::RedirAppend);
646 } else {
647 tokens.push(Token::RedirOut);
648 }
649 } else {
650 tokens.push(Token::RedirOut);
651 }
652 }
653 } else {
654 flush_current_token(&mut current, &mut tokens);
656 chars.next();
657 if let Some(&next_ch) = chars.peek() {
658 if next_ch == '>' {
659 chars.next();
660 tokens.push(Token::RedirAppend);
661 } else {
662 tokens.push(Token::RedirOut);
663 }
664 } else {
665 tokens.push(Token::RedirOut);
666 }
667 }
668 }
669 '<' if !in_double_quote && !in_single_quote => {
670 flush_current_token(&mut current, &mut tokens);
671 chars.next(); if let Some(&'<') = chars.peek() {
673 chars.next(); if let Some(&'<') = chars.peek() {
676 chars.next(); skip_whitespace(&mut chars);
679
680 let mut content = String::new();
681 let mut in_quote = false;
682 let mut quote_char = ' ';
683
684 while let Some(&ch) = chars.peek() {
685 if ch == '\n' && !in_quote {
686 break;
687 }
688 if (ch == '"' || ch == '\'') && !in_quote {
689 in_quote = true;
690 quote_char = ch;
691 chars.next(); } else if in_quote && ch == quote_char {
693 in_quote = false;
694 chars.next(); } else if !in_quote && (ch == ' ' || ch == '\t') {
696 break;
697 } else {
698 content.push(ch);
699 chars.next();
700 }
701 }
702
703 if !content.is_empty() {
704 tokens.push(Token::RedirHereString(content));
705 } else {
706 return Err("Invalid here-string syntax: expected content after <<<"
707 .to_string());
708 }
709 } else {
710 skip_whitespace(&mut chars);
712
713 let mut delimiter = String::new();
714 let mut in_quote = false;
715 let mut quote_char = ' ';
716 let mut was_quoted = false; while let Some(&ch) = chars.peek() {
719 if ch == '\n' && !in_quote {
720 break;
721 }
722 if (ch == '"' || ch == '\'') && !in_quote {
723 in_quote = true;
724 quote_char = ch;
725 was_quoted = true; chars.next(); } else if in_quote && ch == quote_char {
728 in_quote = false;
729 chars.next(); } else if !in_quote && (ch == ' ' || ch == '\t') {
731 break;
732 } else {
733 delimiter.push(ch);
734 chars.next();
735 }
736 }
737
738 if !delimiter.is_empty() {
739 tokens.push(Token::RedirHereDoc(delimiter, was_quoted));
741 } else {
742 return Err(
743 "Invalid here-document syntax: expected delimiter after <<"
744 .to_string(),
745 );
746 }
747 }
748 } else {
749 tokens.push(Token::RedirIn);
751 }
752 }
753 ')' if !in_double_quote && !in_single_quote => {
754 flush_current_token(&mut current, &mut tokens);
755 tokens.push(Token::RightParen);
756 chars.next();
757 }
758 '}' if !in_double_quote && !in_single_quote => {
759 flush_current_token(&mut current, &mut tokens);
760 tokens.push(Token::RightBrace);
761 chars.next();
762 }
763 '(' if !in_double_quote && !in_single_quote => {
764 flush_current_token(&mut current, &mut tokens);
765 tokens.push(Token::LeftParen);
766 chars.next();
767 }
768 '{' if !in_double_quote && !in_single_quote => {
769 let mut temp_chars = chars.clone();
771 let mut brace_content = String::new();
772 let mut depth = 1;
773
774 temp_chars.next(); while let Some(&ch) = temp_chars.peek() {
777 if ch == '{' {
778 depth += 1;
779 } else if ch == '}' {
780 depth -= 1;
781 if depth == 0 {
782 break;
783 }
784 }
785 brace_content.push(ch);
786 temp_chars.next();
787 }
788
789 if depth == 0 && !brace_content.trim().is_empty() {
790 if brace_content.contains(',') || brace_content.contains("..") {
793 current.push('{');
795 current.push_str(&brace_content);
796 current.push('}');
797 chars.next(); let mut content_depth = 1;
800 while let Some(&ch) = chars.peek() {
801 chars.next();
802 if ch == '{' {
803 content_depth += 1;
804 } else if ch == '}' {
805 content_depth -= 1;
806 if content_depth == 0 {
807 break;
808 }
809 }
810 }
811 } else {
812 flush_current_token(&mut current, &mut tokens);
814 tokens.push(Token::LeftBrace);
815 chars.next();
816 }
817 } else {
818 flush_current_token(&mut current, &mut tokens);
820 tokens.push(Token::LeftBrace);
821 chars.next();
822 }
823 }
824 '`' => {
825 flush_current_token(&mut current, &mut tokens);
826 chars.next();
827 let mut sub_command = String::new();
828 while let Some(&ch) = chars.peek() {
829 if ch == '`' {
830 chars.next();
831 break;
832 } else {
833 sub_command.push(ch);
834 chars.next();
835 }
836 }
837 current.push('`');
839 current.push_str(&sub_command);
840 current.push('`');
841 }
842 ';' if !in_double_quote && !in_single_quote => {
843 flush_current_token(&mut current, &mut tokens);
844 chars.next();
845 if let Some(&next_ch) = chars.peek() {
846 if next_ch == ';' {
847 chars.next();
848 tokens.push(Token::DoubleSemicolon);
849 } else {
850 tokens.push(Token::Semicolon);
851 }
852 } else {
853 tokens.push(Token::Semicolon);
854 }
855 }
856 _ => {
857 if ch == '~' && current.is_empty() && !in_single_quote && !in_double_quote {
861 chars.next(); if let Some(&next_ch) = chars.peek() {
865 if next_ch == '+' {
866 chars.next(); if let Some(pwd) = shell_state.get_var("PWD").or_else(|| env::var("PWD").ok()) {
869 current.push_str(&pwd);
870 } else if let Ok(pwd) = env::current_dir() {
871 current.push_str(&pwd.to_string_lossy());
872 } else {
873 current.push_str("~+");
874 }
875 } else if next_ch == '-' {
876 chars.next(); if let Some(oldpwd) = shell_state.get_var("OLDPWD").or_else(|| env::var("OLDPWD").ok()) {
879 current.push_str(&oldpwd);
880 } else {
881 current.push_str("~-");
882 }
883 } else if next_ch == '/' || next_ch == ' ' || next_ch == '\t' || next_ch == '\n' {
884 if let Ok(home) = env::var("HOME") {
886 current.push_str(&home);
887 } else {
888 current.push('~');
889 }
890 } else {
891 let mut username = String::new();
893 while let Some(&ch) = chars.peek() {
894 if ch == '/' || ch == ' ' || ch == '\t' || ch == '\n' {
895 break;
896 }
897 username.push(ch);
898 chars.next();
899 }
900
901 if !username.is_empty() {
902 let user_home = if username == "root" {
905 "/root".to_string()
906 } else {
907 format!("/home/{}", username)
908 };
909
910 if std::path::Path::new(&user_home).exists() {
912 current.push_str(&user_home);
913 } else {
914 current.push('~');
916 current.push_str(&username);
917 }
918 } else {
919 if let Ok(home) = env::var("HOME") {
921 current.push_str(&home);
922 } else {
923 current.push('~');
924 }
925 }
926 }
927 } else {
928 if let Ok(home) = env::var("HOME") {
930 current.push_str(&home);
931 } else {
932 current.push('~');
933 }
934 }
935 } else {
936 current.push(ch);
937 chars.next();
938 }
939 }
940 }
941 }
942 flush_current_token(&mut current, &mut tokens);
943
944 Ok(tokens)
945}
946
947pub fn expand_aliases(
949 tokens: Vec<Token>,
950 shell_state: &ShellState,
951 expanded: &mut HashSet<String>,
952) -> Result<Vec<Token>, String> {
953 if tokens.is_empty() {
954 return Ok(tokens);
955 }
956
957 if let Token::Word(ref word) = tokens[0] {
959 if let Some(alias_value) = shell_state.get_alias(word) {
960 if expanded.contains(word) {
962 return Err(format!("Alias '{}' recursion detected", word));
963 }
964
965 expanded.insert(word.clone());
967
968 let alias_tokens = lex(alias_value, shell_state)?;
970
971 let expanded_alias_tokens = if !alias_tokens.is_empty() {
979 if let Token::Word(ref first_word) = alias_tokens[0] {
980 if first_word != word
982 && shell_state.get_alias(first_word).is_some()
983 && !expanded.contains(first_word)
984 {
985 expand_aliases(alias_tokens, shell_state, expanded)?
986 } else {
987 alias_tokens
988 }
989 } else {
990 alias_tokens
991 }
992 } else {
993 alias_tokens
994 };
995
996 expanded.remove(word);
998
999 let mut result = expanded_alias_tokens;
1001 result.extend_from_slice(&tokens[1..]);
1002 Ok(result)
1003 } else {
1004 Ok(tokens)
1006 }
1007 } else {
1008 Ok(tokens)
1010 }
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015 use super::*;
1016 use std::sync::Mutex;
1017
1018 static ENV_LOCK: Mutex<()> = Mutex::new(());
1020
1021 fn expand_tokens(tokens: Vec<Token>, shell_state: &mut ShellState) -> Vec<Token> {
1024 let mut result = Vec::new();
1025 for token in tokens {
1026 match token {
1027 Token::Word(word) => {
1028 let expanded = crate::executor::expand_variables_in_string(&word, shell_state);
1030 if !expanded.is_empty() || !word.starts_with("$(") {
1033 result.push(Token::Word(expanded));
1034 }
1035 }
1036 other => result.push(other),
1037 }
1038 }
1039 result
1040 }
1041
1042 #[test]
1043 fn test_basic_word() {
1044 let shell_state = ShellState::new();
1045 let result = lex("ls", &shell_state).unwrap();
1046 assert_eq!(result, vec![Token::Word("ls".to_string())]);
1047 }
1048
1049 #[test]
1050 fn test_multiple_words() {
1051 let shell_state = ShellState::new();
1052 let result = lex("ls -la", &shell_state).unwrap();
1053 assert_eq!(
1054 result,
1055 vec![
1056 Token::Word("ls".to_string()),
1057 Token::Word("-la".to_string())
1058 ]
1059 );
1060 }
1061
1062 #[test]
1063 fn test_pipe() {
1064 let shell_state = ShellState::new();
1065 let result = lex("ls | grep txt", &shell_state).unwrap();
1066 assert_eq!(
1067 result,
1068 vec![
1069 Token::Word("ls".to_string()),
1070 Token::Pipe,
1071 Token::Word("grep".to_string()),
1072 Token::Word("txt".to_string())
1073 ]
1074 );
1075 }
1076
1077 #[test]
1078 fn test_redirections() {
1079 let shell_state = ShellState::new();
1080 let result = lex("printf hello > output.txt", &shell_state).unwrap();
1081 assert_eq!(
1082 result,
1083 vec![
1084 Token::Word("printf".to_string()),
1085 Token::Word("hello".to_string()),
1086 Token::RedirOut,
1087 Token::Word("output.txt".to_string())
1088 ]
1089 );
1090 }
1091
1092 #[test]
1093 fn test_append_redirection() {
1094 let shell_state = ShellState::new();
1095 let result = lex("printf hello >> output.txt", &shell_state).unwrap();
1096 assert_eq!(
1097 result,
1098 vec![
1099 Token::Word("printf".to_string()),
1100 Token::Word("hello".to_string()),
1101 Token::RedirAppend,
1102 Token::Word("output.txt".to_string())
1103 ]
1104 );
1105 }
1106
1107 #[test]
1108 fn test_input_redirection() {
1109 let shell_state = ShellState::new();
1110 let result = lex("cat < input.txt", &shell_state).unwrap();
1111 assert_eq!(
1112 result,
1113 vec![
1114 Token::Word("cat".to_string()),
1115 Token::RedirIn,
1116 Token::Word("input.txt".to_string())
1117 ]
1118 );
1119 }
1120
1121 #[test]
1122 fn test_double_quotes() {
1123 let shell_state = ShellState::new();
1124 let result = lex("echo \"hello world\"", &shell_state).unwrap();
1125 assert_eq!(
1126 result,
1127 vec![
1128 Token::Word("echo".to_string()),
1129 Token::Word("hello world".to_string())
1130 ]
1131 );
1132 }
1133
1134 #[test]
1135 fn test_single_quotes() {
1136 let shell_state = ShellState::new();
1137 let result = lex("echo 'hello world'", &shell_state).unwrap();
1138 assert_eq!(
1139 result,
1140 vec![
1141 Token::Word("echo".to_string()),
1142 Token::Word("hello world".to_string())
1143 ]
1144 );
1145 }
1146
1147 #[test]
1148 fn test_variable_expansion() {
1149 let mut shell_state = ShellState::new();
1150 shell_state.set_var("TEST_VAR", "expanded_value".to_string());
1151 let tokens = lex("echo $TEST_VAR", &shell_state).unwrap();
1152 let result = expand_tokens(tokens, &mut shell_state);
1153 assert_eq!(
1154 result,
1155 vec![
1156 Token::Word("echo".to_string()),
1157 Token::Word("expanded_value".to_string())
1158 ]
1159 );
1160 }
1161
1162 #[test]
1163 fn test_variable_expansion_nonexistent() {
1164 let shell_state = ShellState::new();
1165 let result = lex("echo $TEST_VAR2", &shell_state).unwrap();
1166 assert_eq!(
1167 result,
1168 vec![
1169 Token::Word("echo".to_string()),
1170 Token::Word("$TEST_VAR2".to_string())
1171 ]
1172 );
1173 }
1174
1175 #[test]
1176 fn test_empty_variable() {
1177 let shell_state = ShellState::new();
1178 let result = lex("echo $", &shell_state).unwrap();
1179 assert_eq!(
1180 result,
1181 vec![
1182 Token::Word("echo".to_string()),
1183 Token::Word("$".to_string())
1184 ]
1185 );
1186 }
1187
1188 #[test]
1189 fn test_mixed_quotes_and_variables() {
1190 let mut shell_state = ShellState::new();
1191 shell_state.set_var("USER", "alice".to_string());
1192 let tokens = lex("echo \"Hello $USER\"", &shell_state).unwrap();
1193 let result = expand_tokens(tokens, &mut shell_state);
1194 assert_eq!(
1195 result,
1196 vec![
1197 Token::Word("echo".to_string()),
1198 Token::Word("Hello alice".to_string())
1199 ]
1200 );
1201 }
1202
1203 #[test]
1204 fn test_unclosed_double_quote() {
1205 let shell_state = ShellState::new();
1207 let result = lex("echo \"hello", &shell_state).unwrap();
1208 assert_eq!(
1209 result,
1210 vec![
1211 Token::Word("echo".to_string()),
1212 Token::Word("hello".to_string())
1213 ]
1214 );
1215 }
1216
1217 #[test]
1218 fn test_empty_input() {
1219 let shell_state = ShellState::new();
1220 let result = lex("", &shell_state).unwrap();
1221 assert_eq!(result, Vec::<Token>::new());
1222 }
1223
1224 #[test]
1225 fn test_only_spaces() {
1226 let shell_state = ShellState::new();
1227 let result = lex(" ", &shell_state).unwrap();
1228 assert_eq!(result, Vec::<Token>::new());
1229 }
1230
1231 #[test]
1232 fn test_complex_pipeline() {
1233 let shell_state = ShellState::new();
1234 let result = lex(
1235 "cat input.txt | grep \"search term\" > output.txt",
1236 &shell_state,
1237 )
1238 .unwrap();
1239 assert_eq!(
1240 result,
1241 vec![
1242 Token::Word("cat".to_string()),
1243 Token::Word("input.txt".to_string()),
1244 Token::Pipe,
1245 Token::Word("grep".to_string()),
1246 Token::Word("search term".to_string()),
1247 Token::RedirOut,
1248 Token::Word("output.txt".to_string())
1249 ]
1250 );
1251 }
1252
1253 #[test]
1254 fn test_if_tokens() {
1255 let shell_state = ShellState::new();
1256 let result = lex("if true; then printf yes; fi", &shell_state).unwrap();
1257 assert_eq!(
1258 result,
1259 vec![
1260 Token::If,
1261 Token::Word("true".to_string()),
1262 Token::Semicolon,
1263 Token::Then,
1264 Token::Word("printf".to_string()),
1265 Token::Word("yes".to_string()),
1266 Token::Semicolon,
1267 Token::Fi,
1268 ]
1269 );
1270 }
1271
1272 #[test]
1273 fn test_command_substitution_dollar_paren() {
1274 let shell_state = ShellState::new();
1275 let result = lex("echo $(pwd)", &shell_state).unwrap();
1276 assert_eq!(result.len(), 2);
1278 assert_eq!(result[0], Token::Word("echo".to_string()));
1279 assert!(matches!(result[1], Token::Word(_)));
1280 }
1281
1282 #[test]
1283 fn test_command_substitution_backticks() {
1284 let shell_state = ShellState::new();
1285 let result = lex("echo `pwd`", &shell_state).unwrap();
1286 assert_eq!(result.len(), 2);
1288 assert_eq!(result[0], Token::Word("echo".to_string()));
1289 assert!(matches!(result[1], Token::Word(_)));
1290 }
1291
1292 #[test]
1293 fn test_command_substitution_with_arguments() {
1294 let mut shell_state = ShellState::new();
1295 let tokens = lex("echo $(echo hello world)", &shell_state).unwrap();
1296 let result = expand_tokens(tokens, &mut shell_state);
1297 assert_eq!(
1298 result,
1299 vec![
1300 Token::Word("echo".to_string()),
1301 Token::Word("hello world".to_string())
1302 ]
1303 );
1304 }
1305
1306 #[test]
1307 fn test_command_substitution_backticks_with_arguments() {
1308 let mut shell_state = ShellState::new();
1309 let tokens = lex("echo `echo hello world`", &shell_state).unwrap();
1310 let result = expand_tokens(tokens, &mut shell_state);
1311 assert_eq!(
1312 result,
1313 vec![
1314 Token::Word("echo".to_string()),
1315 Token::Word("hello world".to_string())
1316 ]
1317 );
1318 }
1319
1320 #[test]
1321 fn test_command_substitution_failure_fallback() {
1322 let shell_state = ShellState::new();
1323 let result = lex("echo $(nonexistent_command)", &shell_state).unwrap();
1324 assert_eq!(
1325 result,
1326 vec![
1327 Token::Word("echo".to_string()),
1328 Token::Word("$(nonexistent_command)".to_string())
1329 ]
1330 );
1331 }
1332
1333 #[test]
1334 fn test_command_substitution_backticks_failure_fallback() {
1335 let shell_state = ShellState::new();
1336 let result = lex("echo `nonexistent_command`", &shell_state).unwrap();
1337 assert_eq!(
1338 result,
1339 vec![
1340 Token::Word("echo".to_string()),
1341 Token::Word("`nonexistent_command`".to_string())
1342 ]
1343 );
1344 }
1345
1346 #[test]
1347 fn test_command_substitution_with_variables() {
1348 let mut shell_state = ShellState::new();
1349 shell_state.set_var("TEST_VAR", "test_value".to_string());
1350 let tokens = lex("echo $(echo $TEST_VAR)", &shell_state).unwrap();
1351 let result = expand_tokens(tokens, &mut shell_state);
1352 assert_eq!(
1353 result,
1354 vec![
1355 Token::Word("echo".to_string()),
1356 Token::Word("test_value".to_string())
1357 ]
1358 );
1359 }
1360
1361 #[test]
1362 fn test_command_substitution_in_assignment() {
1363 let mut shell_state = ShellState::new();
1364 let tokens = lex("MY_VAR=$(echo hello)", &shell_state).unwrap();
1365 let result = expand_tokens(tokens, &mut shell_state);
1366 assert_eq!(result, vec![Token::Word("MY_VAR=hello".to_string())]);
1368 }
1369
1370 #[test]
1371 fn test_command_substitution_backticks_in_assignment() {
1372 let mut shell_state = ShellState::new();
1373 let tokens = lex("MY_VAR=`echo hello`", &shell_state).unwrap();
1374 let result = expand_tokens(tokens, &mut shell_state);
1375 assert_eq!(
1377 result,
1378 vec![
1379 Token::Word("MY_VAR=".to_string()),
1380 Token::Word("hello".to_string())
1381 ]
1382 );
1383 }
1384
1385 #[test]
1386 fn test_command_substitution_with_quotes() {
1387 let mut shell_state = ShellState::new();
1388 let tokens = lex("echo \"$(echo hello world)\"", &shell_state).unwrap();
1389 let result = expand_tokens(tokens, &mut shell_state);
1390 assert_eq!(
1391 result,
1392 vec![
1393 Token::Word("echo".to_string()),
1394 Token::Word("hello world".to_string())
1395 ]
1396 );
1397 }
1398
1399 #[test]
1400 fn test_command_substitution_backticks_with_quotes() {
1401 let mut shell_state = ShellState::new();
1402 let tokens = lex("echo \"`echo hello world`\"", &shell_state).unwrap();
1403 let result = expand_tokens(tokens, &mut shell_state);
1404 assert_eq!(
1405 result,
1406 vec![
1407 Token::Word("echo".to_string()),
1408 Token::Word("hello world".to_string())
1409 ]
1410 );
1411 }
1412
1413 #[test]
1414 fn test_command_substitution_empty_output() {
1415 let mut shell_state = ShellState::new();
1416 let tokens = lex("echo $(true)", &shell_state).unwrap();
1417 let result = expand_tokens(tokens, &mut shell_state);
1418 assert_eq!(result, vec![Token::Word("echo".to_string())]);
1420 }
1421
1422 #[test]
1423 fn test_command_substitution_multiple_spaces() {
1424 let mut shell_state = ShellState::new();
1425 let tokens = lex("echo $(echo 'hello world')", &shell_state).unwrap();
1426 let result = expand_tokens(tokens, &mut shell_state);
1427 assert_eq!(
1428 result,
1429 vec![
1430 Token::Word("echo".to_string()),
1431 Token::Word("hello world".to_string())
1432 ]
1433 );
1434 }
1435
1436 #[test]
1437 fn test_command_substitution_with_newlines() {
1438 let mut shell_state = ShellState::new();
1439 let tokens = lex("echo $(printf 'hello\nworld')", &shell_state).unwrap();
1440 let result = expand_tokens(tokens, &mut shell_state);
1441 assert_eq!(
1442 result,
1443 vec![
1444 Token::Word("echo".to_string()),
1445 Token::Word("hello\nworld".to_string())
1446 ]
1447 );
1448 }
1449
1450 #[test]
1451 fn test_command_substitution_special_characters() {
1452 let shell_state = ShellState::new();
1453 let result = lex("echo $(echo '$#@^&*()')", &shell_state).unwrap();
1454 println!("Special chars test result: {:?}", result);
1455 assert_eq!(result.len(), 2);
1458 assert_eq!(result[0], Token::Word("echo".to_string()));
1459 assert!(matches!(result[1], Token::Word(_)));
1460 }
1461
1462 #[test]
1463 fn test_nested_command_substitution() {
1464 let shell_state = ShellState::new();
1467 let result = lex("echo $(echo $(pwd))", &shell_state).unwrap();
1468 assert_eq!(result.len(), 2);
1470 assert_eq!(result[0], Token::Word("echo".to_string()));
1471 assert!(matches!(result[1], Token::Word(_)));
1472 }
1473
1474 #[test]
1475 fn test_command_substitution_in_pipeline() {
1476 let shell_state = ShellState::new();
1477 let result = lex("$(echo hello) | cat", &shell_state).unwrap();
1478 println!("Pipeline test result: {:?}", result);
1479 assert_eq!(result.len(), 3);
1480 assert!(matches!(result[0], Token::Word(_)));
1481 assert_eq!(result[1], Token::Pipe);
1482 assert_eq!(result[2], Token::Word("cat".to_string()));
1483 }
1484
1485 #[test]
1486 fn test_command_substitution_with_redirection() {
1487 let shell_state = ShellState::new();
1488 let result = lex("$(echo hello) > output.txt", &shell_state).unwrap();
1489 assert_eq!(result.len(), 3);
1490 assert!(matches!(result[0], Token::Word(_)));
1491 assert_eq!(result[1], Token::RedirOut);
1492 assert_eq!(result[2], Token::Word("output.txt".to_string()));
1493 }
1494
1495 #[test]
1496 fn test_variable_in_quotes_with_pipe() {
1497 let mut shell_state = ShellState::new();
1498 shell_state.set_var("PATH", "/usr/bin:/bin".to_string());
1499 let tokens = lex("echo \"$PATH\" | tr ':' '\\n'", &shell_state).unwrap();
1500 let result = expand_tokens(tokens, &mut shell_state);
1501 assert_eq!(
1502 result,
1503 vec![
1504 Token::Word("echo".to_string()),
1505 Token::Word("/usr/bin:/bin".to_string()),
1506 Token::Pipe,
1507 Token::Word("tr".to_string()),
1508 Token::Word(":".to_string()),
1509 Token::Word("\\n".to_string())
1510 ]
1511 );
1512 }
1513
1514 #[test]
1515 fn test_expand_aliases_simple() {
1516 let mut shell_state = ShellState::new();
1517 shell_state.set_alias("ll", "ls -l".to_string());
1518 let tokens = vec![Token::Word("ll".to_string())];
1519 let result = expand_aliases(tokens, &shell_state, &mut HashSet::new()).unwrap();
1520 assert_eq!(
1521 result,
1522 vec![Token::Word("ls".to_string()), Token::Word("-l".to_string())]
1523 );
1524 }
1525
1526 #[test]
1527 fn test_expand_aliases_with_args() {
1528 let mut shell_state = ShellState::new();
1529 shell_state.set_alias("ll", "ls -l".to_string());
1530 let tokens = vec![
1531 Token::Word("ll".to_string()),
1532 Token::Word("/tmp".to_string()),
1533 ];
1534 let result = expand_aliases(tokens, &shell_state, &mut HashSet::new()).unwrap();
1535 assert_eq!(
1536 result,
1537 vec![
1538 Token::Word("ls".to_string()),
1539 Token::Word("-l".to_string()),
1540 Token::Word("/tmp".to_string())
1541 ]
1542 );
1543 }
1544
1545 #[test]
1546 fn test_expand_aliases_no_alias() {
1547 let shell_state = ShellState::new();
1548 let tokens = vec![Token::Word("ls".to_string())];
1549 let result = expand_aliases(tokens.clone(), &shell_state, &mut HashSet::new()).unwrap();
1550 assert_eq!(result, tokens);
1551 }
1552
1553 #[test]
1554 fn test_expand_aliases_chained() {
1555 let mut shell_state = ShellState::new();
1559 shell_state.set_alias("a", "b".to_string());
1560 shell_state.set_alias("b", "a".to_string());
1561 let tokens = vec![Token::Word("a".to_string())];
1562 let result = expand_aliases(tokens, &shell_state, &mut HashSet::new());
1563 assert!(result.is_ok());
1565 assert_eq!(result.unwrap(), vec![Token::Word("a".to_string())]);
1566 }
1567
1568 #[test]
1569 fn test_arithmetic_expansion_simple() {
1570 let mut shell_state = ShellState::new();
1571 let tokens = lex("echo $((2 + 3))", &shell_state).unwrap();
1572 let result = expand_tokens(tokens, &mut shell_state);
1573 assert_eq!(
1574 result,
1575 vec![
1576 Token::Word("echo".to_string()),
1577 Token::Word("5".to_string())
1578 ]
1579 );
1580 }
1581
1582 #[test]
1583 fn test_arithmetic_expansion_with_variables() {
1584 let mut shell_state = ShellState::new();
1585 shell_state.set_var("x", "10".to_string());
1586 shell_state.set_var("y", "20".to_string());
1587 let tokens = lex("echo $((x + y * 2))", &shell_state).unwrap();
1588 let result = expand_tokens(tokens, &mut shell_state);
1589 assert_eq!(
1590 result,
1591 vec![
1592 Token::Word("echo".to_string()),
1593 Token::Word("50".to_string()) ]
1595 );
1596 }
1597
1598 #[test]
1599 fn test_arithmetic_expansion_comparison() {
1600 let mut shell_state = ShellState::new();
1601 let tokens = lex("echo $((5 > 3))", &shell_state).unwrap();
1602 let result = expand_tokens(tokens, &mut shell_state);
1603 assert_eq!(
1604 result,
1605 vec![
1606 Token::Word("echo".to_string()),
1607 Token::Word("1".to_string()) ]
1609 );
1610 }
1611
1612 #[test]
1613 fn test_arithmetic_expansion_complex() {
1614 let mut shell_state = ShellState::new();
1615 shell_state.set_var("a", "3".to_string());
1616 let tokens = lex("echo $((a * 2 + 5))", &shell_state).unwrap();
1617 let result = expand_tokens(tokens, &mut shell_state);
1618 assert_eq!(
1619 result,
1620 vec![
1621 Token::Word("echo".to_string()),
1622 Token::Word("11".to_string()) ]
1624 );
1625 }
1626
1627 #[test]
1628 fn test_arithmetic_expansion_unmatched_parentheses() {
1629 let mut shell_state = ShellState::new();
1630 let tokens = lex("echo $((2 + 3", &shell_state).unwrap();
1631 let result = expand_tokens(tokens, &mut shell_state);
1632 assert_eq!(result.len(), 2);
1634 assert_eq!(result[0], Token::Word("echo".to_string()));
1635 let second_token = &result[1];
1637 if let Token::Word(s) = second_token {
1638 assert!(
1639 s.starts_with("$((") && s.contains("2") && s.contains("3"),
1640 "Expected unmatched arithmetic to be kept as literal, got: {}",
1641 s
1642 );
1643 } else {
1644 panic!("Expected Word token");
1645 }
1646 }
1647
1648 #[test]
1649 fn test_arithmetic_expansion_division_by_zero() {
1650 let mut shell_state = ShellState::new();
1651 let tokens = lex("echo $((5 / 0))", &shell_state).unwrap();
1652 let result = expand_tokens(tokens, &mut shell_state);
1653 assert_eq!(result.len(), 2);
1655 assert_eq!(result[0], Token::Word("echo".to_string()));
1656 if let Token::Word(s) = &result[1] {
1658 assert!(
1659 s.contains("Division by zero"),
1660 "Expected division by zero error, got: {}",
1661 s
1662 );
1663 } else {
1664 panic!("Expected Word token");
1665 }
1666 }
1667
1668 #[test]
1669 fn test_parameter_expansion_simple() {
1670 let mut shell_state = ShellState::new();
1671 shell_state.set_var("TEST_VAR", "hello world".to_string());
1672 let result = lex("echo ${TEST_VAR}", &shell_state).unwrap();
1673 assert_eq!(
1674 result,
1675 vec![
1676 Token::Word("echo".to_string()),
1677 Token::Word("hello world".to_string())
1678 ]
1679 );
1680 }
1681
1682 #[test]
1683 fn test_parameter_expansion_unset_variable() {
1684 let shell_state = ShellState::new();
1685 let result = lex("echo ${UNSET_VAR}", &shell_state).unwrap();
1686 assert_eq!(
1687 result,
1688 vec![Token::Word("echo".to_string()), Token::Word("".to_string())]
1689 );
1690 }
1691
1692 #[test]
1693 fn test_parameter_expansion_default() {
1694 let shell_state = ShellState::new();
1695 let result = lex("echo ${UNSET_VAR:-default}", &shell_state).unwrap();
1696 assert_eq!(
1697 result,
1698 vec![
1699 Token::Word("echo".to_string()),
1700 Token::Word("default".to_string())
1701 ]
1702 );
1703 }
1704
1705 #[test]
1706 fn test_parameter_expansion_default_set_variable() {
1707 let mut shell_state = ShellState::new();
1708 shell_state.set_var("TEST_VAR", "value".to_string());
1709 let result = lex("echo ${TEST_VAR:-default}", &shell_state).unwrap();
1710 assert_eq!(
1711 result,
1712 vec![
1713 Token::Word("echo".to_string()),
1714 Token::Word("value".to_string())
1715 ]
1716 );
1717 }
1718
1719 #[test]
1720 fn test_parameter_expansion_assign_default() {
1721 let shell_state = ShellState::new();
1722 let result = lex("echo ${UNSET_VAR:=default}", &shell_state).unwrap();
1723 assert_eq!(
1724 result,
1725 vec![
1726 Token::Word("echo".to_string()),
1727 Token::Word("default".to_string())
1728 ]
1729 );
1730 }
1731
1732 #[test]
1733 fn test_parameter_expansion_alternative() {
1734 let mut shell_state = ShellState::new();
1735 shell_state.set_var("TEST_VAR", "value".to_string());
1736 let result = lex("echo ${TEST_VAR:+replacement}", &shell_state).unwrap();
1737 assert_eq!(
1738 result,
1739 vec![
1740 Token::Word("echo".to_string()),
1741 Token::Word("replacement".to_string())
1742 ]
1743 );
1744 }
1745
1746 #[test]
1747 fn test_parameter_expansion_alternative_unset() {
1748 let shell_state = ShellState::new();
1749 let result = lex("echo ${UNSET_VAR:+replacement}", &shell_state).unwrap();
1750 assert_eq!(
1751 result,
1752 vec![Token::Word("echo".to_string()), Token::Word("".to_string())]
1753 );
1754 }
1755
1756 #[test]
1757 fn test_parameter_expansion_substring() {
1758 let mut shell_state = ShellState::new();
1759 shell_state.set_var("TEST_VAR", "hello world".to_string());
1760 let result = lex("echo ${TEST_VAR:6}", &shell_state).unwrap();
1761 assert_eq!(
1762 result,
1763 vec![
1764 Token::Word("echo".to_string()),
1765 Token::Word("world".to_string())
1766 ]
1767 );
1768 }
1769
1770 #[test]
1771 fn test_parameter_expansion_substring_with_length() {
1772 let mut shell_state = ShellState::new();
1773 shell_state.set_var("TEST_VAR", "hello world".to_string());
1774 let result = lex("echo ${TEST_VAR:0:5}", &shell_state).unwrap();
1775 assert_eq!(
1776 result,
1777 vec![
1778 Token::Word("echo".to_string()),
1779 Token::Word("hello".to_string())
1780 ]
1781 );
1782 }
1783
1784 #[test]
1785 fn test_parameter_expansion_length() {
1786 let mut shell_state = ShellState::new();
1787 shell_state.set_var("TEST_VAR", "hello".to_string());
1788 let result = lex("echo ${#TEST_VAR}", &shell_state).unwrap();
1789 assert_eq!(
1790 result,
1791 vec![
1792 Token::Word("echo".to_string()),
1793 Token::Word("5".to_string())
1794 ]
1795 );
1796 }
1797
1798 #[test]
1799 fn test_parameter_expansion_remove_shortest_prefix() {
1800 let mut shell_state = ShellState::new();
1801 shell_state.set_var("TEST_VAR", "prefix_hello".to_string());
1802 let result = lex("echo ${TEST_VAR#prefix_}", &shell_state).unwrap();
1803 assert_eq!(
1804 result,
1805 vec![
1806 Token::Word("echo".to_string()),
1807 Token::Word("hello".to_string())
1808 ]
1809 );
1810 }
1811
1812 #[test]
1813 fn test_parameter_expansion_remove_longest_prefix() {
1814 let mut shell_state = ShellState::new();
1815 shell_state.set_var("TEST_VAR", "prefix_prefix_hello".to_string());
1816 let result = lex("echo ${TEST_VAR##prefix_}", &shell_state).unwrap();
1817 assert_eq!(
1818 result,
1819 vec![
1820 Token::Word("echo".to_string()),
1821 Token::Word("prefix_hello".to_string())
1822 ]
1823 );
1824 }
1825
1826 #[test]
1827 fn test_parameter_expansion_remove_shortest_suffix() {
1828 let mut shell_state = ShellState::new();
1829 shell_state.set_var("TEST_VAR", "hello_suffix".to_string());
1830 let result = lex("echo ${TEST_VAR%suffix}", &shell_state).unwrap();
1831 assert_eq!(
1832 result,
1833 vec![
1834 Token::Word("echo".to_string()),
1835 Token::Word("hello_".to_string()) ]
1837 );
1838 }
1839
1840 #[test]
1841 fn test_parameter_expansion_remove_longest_suffix() {
1842 let mut shell_state = ShellState::new();
1843 shell_state.set_var("TEST_VAR", "hello_suffix_suffix".to_string());
1844 let result = lex("echo ${TEST_VAR%%suffix}", &shell_state).unwrap();
1845 assert_eq!(
1846 result,
1847 vec![
1848 Token::Word("echo".to_string()),
1849 Token::Word("hello_suffix_".to_string()) ]
1851 );
1852 }
1853
1854 #[test]
1855 fn test_parameter_expansion_substitute() {
1856 let mut shell_state = ShellState::new();
1857 shell_state.set_var("TEST_VAR", "hello world".to_string());
1858 let result = lex("echo ${TEST_VAR/world/universe}", &shell_state).unwrap();
1859 assert_eq!(
1860 result,
1861 vec![
1862 Token::Word("echo".to_string()),
1863 Token::Word("hello universe".to_string())
1864 ]
1865 );
1866 }
1867
1868 #[test]
1869 fn test_parameter_expansion_substitute_all() {
1870 let mut shell_state = ShellState::new();
1871 shell_state.set_var("TEST_VAR", "hello world world".to_string());
1872 let result = lex("echo ${TEST_VAR//world/universe}", &shell_state).unwrap();
1873 assert_eq!(
1874 result,
1875 vec![
1876 Token::Word("echo".to_string()),
1877 Token::Word("hello universe universe".to_string())
1878 ]
1879 );
1880 }
1881
1882 #[test]
1883 fn test_parameter_expansion_mixed_with_regular_variables() {
1884 let mut shell_state = ShellState::new();
1885 shell_state.set_var("VAR1", "value1".to_string());
1886 shell_state.set_var("VAR2", "value2".to_string());
1887 let tokens = lex("echo $VAR1 and ${VAR2}", &shell_state).unwrap();
1888 let result = expand_tokens(tokens, &mut shell_state);
1889 assert_eq!(
1890 result,
1891 vec![
1892 Token::Word("echo".to_string()),
1893 Token::Word("value1".to_string()),
1894 Token::Word("and".to_string()),
1895 Token::Word("value2".to_string())
1896 ]
1897 );
1898 }
1899
1900 #[test]
1901 fn test_parameter_expansion_in_double_quotes() {
1902 let mut shell_state = ShellState::new();
1903 shell_state.set_var("TEST_VAR", "hello".to_string());
1904 let result = lex("echo \"Value: ${TEST_VAR}\"", &shell_state).unwrap();
1905 assert_eq!(
1906 result,
1907 vec![
1908 Token::Word("echo".to_string()),
1909 Token::Word("Value: hello".to_string())
1910 ]
1911 );
1912 }
1913
1914 #[test]
1915 fn test_parameter_expansion_error_unset() {
1916 let shell_state = ShellState::new();
1917 let result = lex("echo ${UNSET_VAR:?error message}", &shell_state);
1918 assert!(result.is_ok());
1920 let tokens = result.unwrap();
1921 assert_eq!(tokens.len(), 3);
1922 assert_eq!(tokens[0], Token::Word("echo".to_string()));
1923 assert_eq!(tokens[1], Token::Word("${UNSET_VAR:?error}".to_string()));
1924 assert_eq!(tokens[2], Token::Word("message}".to_string()));
1925 }
1926
1927 #[test]
1928 fn test_parameter_expansion_complex_expression() {
1929 let mut shell_state = ShellState::new();
1930 shell_state.set_var("PATH", "/usr/bin:/bin:/usr/local/bin".to_string());
1931 let result = lex("echo ${PATH#/usr/bin:}", &shell_state).unwrap();
1932 assert_eq!(
1933 result,
1934 vec![
1935 Token::Word("echo".to_string()),
1936 Token::Word("/bin:/usr/local/bin".to_string())
1937 ]
1938 );
1939 }
1940
1941 #[test]
1942 fn test_local_keyword() {
1943 let shell_state = ShellState::new();
1944 let result = lex("local myvar", &shell_state).unwrap();
1945 assert_eq!(result, vec![Token::Local, Token::Word("myvar".to_string())]);
1946 }
1947
1948 #[test]
1949 fn test_local_keyword_in_function() {
1950 let shell_state = ShellState::new();
1951 let result = lex("local var=value", &shell_state).unwrap();
1952 assert_eq!(
1953 result,
1954 vec![Token::Local, Token::Word("var=value".to_string())]
1955 );
1956 }
1957
1958 #[test]
1959 fn test_single_quotes_with_semicolons() {
1960 let shell_state = ShellState::new();
1962 let result = lex("trap 'echo \"A\"; echo \"B\"' EXIT", &shell_state).unwrap();
1963 assert_eq!(
1964 result,
1965 vec![
1966 Token::Word("trap".to_string()),
1967 Token::Word("echo \"A\"; echo \"B\"".to_string()),
1968 Token::Word("EXIT".to_string())
1969 ]
1970 );
1971 }
1972
1973 #[test]
1974 fn test_double_quotes_with_semicolons() {
1975 let shell_state = ShellState::new();
1977 let result = lex("echo \"command1; command2\"", &shell_state).unwrap();
1978 assert_eq!(
1979 result,
1980 vec![
1981 Token::Word("echo".to_string()),
1982 Token::Word("command1; command2".to_string())
1983 ]
1984 );
1985 }
1986
1987 #[test]
1988 fn test_semicolons_outside_quotes() {
1989 let shell_state = ShellState::new();
1991 let result = lex("echo hello; echo world", &shell_state).unwrap();
1992 assert_eq!(
1993 result,
1994 vec![
1995 Token::Word("echo".to_string()),
1996 Token::Word("hello".to_string()),
1997 Token::Semicolon,
1998 Token::Word("echo".to_string()),
1999 Token::Word("world".to_string())
2000 ]
2001 );
2002 }
2003
2004 #[test]
2005 fn test_here_document_redirection() {
2006 let shell_state = ShellState::new();
2007 let result = lex("cat << EOF", &shell_state).unwrap();
2008 assert_eq!(
2009 result,
2010 vec![
2011 Token::Word("cat".to_string()),
2012 Token::RedirHereDoc("EOF".to_string(), false)
2013 ]
2014 );
2015 }
2016
2017 #[test]
2018 fn test_here_string_redirection() {
2019 let shell_state = ShellState::new();
2020 let result = lex("cat <<< \"hello world\"", &shell_state).unwrap();
2021 assert_eq!(
2022 result,
2023 vec![
2024 Token::Word("cat".to_string()),
2025 Token::RedirHereString("hello world".to_string())
2026 ]
2027 );
2028 }
2029
2030 #[test]
2031 fn test_here_document_with_quoted_delimiter() {
2032 let shell_state = ShellState::new();
2033 let result = lex("command << 'EOF'", &shell_state).unwrap();
2034 assert_eq!(
2035 result,
2036 vec![
2037 Token::Word("command".to_string()),
2038 Token::RedirHereDoc("EOF".to_string(), true) ]
2040 );
2041 }
2042
2043 #[test]
2044 fn test_here_string_without_quotes() {
2045 let shell_state = ShellState::new();
2046 let result = lex("grep <<< pattern", &shell_state).unwrap();
2047 assert_eq!(
2048 result,
2049 vec![
2050 Token::Word("grep".to_string()),
2051 Token::RedirHereString("pattern".to_string())
2052 ]
2053 );
2054 }
2055
2056 #[test]
2057 fn test_redirections_mixed() {
2058 let shell_state = ShellState::new();
2059 let result = lex(
2060 "cat < input.txt <<< \"fallback\" > output.txt",
2061 &shell_state,
2062 )
2063 .unwrap();
2064 assert_eq!(
2065 result,
2066 vec![
2067 Token::Word("cat".to_string()),
2068 Token::RedirIn,
2069 Token::Word("input.txt".to_string()),
2070 Token::RedirHereString("fallback".to_string()),
2071 Token::RedirOut,
2072 Token::Word("output.txt".to_string())
2073 ]
2074 );
2075 }
2076
2077 #[test]
2078 fn test_tilde_expansion_unquoted() {
2079 let _lock = ENV_LOCK.lock().unwrap();
2080 let shell_state = ShellState::new();
2081 let home = env::var("HOME").unwrap_or_else(|_| "/home/user".to_string());
2082 let result = lex("echo ~", &shell_state).unwrap();
2083 assert_eq!(
2084 result,
2085 vec![
2086 Token::Word("echo".to_string()),
2087 Token::Word(home)
2088 ]
2089 );
2090 }
2091
2092 #[test]
2093 fn test_tilde_expansion_single_quoted() {
2094 let shell_state = ShellState::new();
2095 let result = lex("echo '~'", &shell_state).unwrap();
2096 assert_eq!(
2097 result,
2098 vec![
2099 Token::Word("echo".to_string()),
2100 Token::Word("~".to_string())
2101 ]
2102 );
2103 }
2104
2105 #[test]
2106 fn test_tilde_expansion_double_quoted() {
2107 let shell_state = ShellState::new();
2108 let result = lex("echo \"~\"", &shell_state).unwrap();
2109 assert_eq!(
2110 result,
2111 vec![
2112 Token::Word("echo".to_string()),
2113 Token::Word("~".to_string())
2114 ]
2115 );
2116 }
2117
2118 #[test]
2119 fn test_tilde_expansion_mixed_quotes() {
2120 let _lock = ENV_LOCK.lock().unwrap();
2121 let shell_state = ShellState::new();
2122 let home = env::var("HOME").unwrap_or_else(|_| "/home/user".to_string());
2123 let result = lex("echo ~ '~' \"~\"", &shell_state).unwrap();
2124 assert_eq!(
2125 result,
2126 vec![
2127 Token::Word("echo".to_string()),
2128 Token::Word(home),
2129 Token::Word("~".to_string()),
2130 Token::Word("~".to_string())
2131 ]
2132 );
2133 }
2134
2135 #[test]
2136 fn test_tilde_expansion_pwd() {
2137 let mut shell_state = ShellState::new();
2138
2139 let test_pwd = "/test/current/dir";
2141 shell_state.set_var("PWD", test_pwd.to_string());
2142
2143 let result = lex("echo ~+", &shell_state).unwrap();
2144 assert_eq!(
2145 result,
2146 vec![
2147 Token::Word("echo".to_string()),
2148 Token::Word(test_pwd.to_string())
2149 ]
2150 );
2151 }
2152
2153 #[test]
2154 fn test_tilde_expansion_oldpwd() {
2155 let mut shell_state = ShellState::new();
2156
2157 let test_oldpwd = "/test/old/dir";
2159 shell_state.set_var("OLDPWD", test_oldpwd.to_string());
2160
2161 let result = lex("echo ~-", &shell_state).unwrap();
2162 assert_eq!(
2163 result,
2164 vec![
2165 Token::Word("echo".to_string()),
2166 Token::Word(test_oldpwd.to_string())
2167 ]
2168 );
2169 }
2170
2171 #[test]
2172 fn test_tilde_expansion_pwd_unset() {
2173 let _lock = ENV_LOCK.lock().unwrap();
2174 let shell_state = ShellState::new();
2175
2176 let result = lex("echo ~+", &shell_state).unwrap();
2178 assert_eq!(result.len(), 2);
2179 assert_eq!(result[0], Token::Word("echo".to_string()));
2180
2181 if let Token::Word(path) = &result[1] {
2183 assert!(path.starts_with('/') || path == "~+");
2185 } else {
2186 panic!("Expected Word token");
2187 }
2188 }
2189
2190 #[test]
2191 fn test_tilde_expansion_oldpwd_unset() {
2192 let _lock = ENV_LOCK.lock().unwrap();
2194
2195 let original_oldpwd = env::var("OLDPWD").ok();
2197 unsafe {
2198 env::remove_var("OLDPWD");
2199 }
2200
2201 let shell_state = ShellState::new();
2202
2203 let result = lex("echo ~-", &shell_state).unwrap();
2205 assert_eq!(
2206 result,
2207 vec![
2208 Token::Word("echo".to_string()),
2209 Token::Word("~-".to_string())
2210 ]
2211 );
2212
2213 unsafe {
2215 if let Some(oldpwd) = original_oldpwd {
2216 env::set_var("OLDPWD", oldpwd);
2217 }
2218 }
2219 }
2220
2221 #[test]
2222 fn test_tilde_expansion_pwd_in_quotes() {
2223 let mut shell_state = ShellState::new();
2224 shell_state.set_var("PWD", "/test/dir".to_string());
2225
2226 let result = lex("echo '~+'", &shell_state).unwrap();
2228 assert_eq!(
2229 result,
2230 vec![
2231 Token::Word("echo".to_string()),
2232 Token::Word("~+".to_string())
2233 ]
2234 );
2235
2236 let result = lex("echo \"~+\"", &shell_state).unwrap();
2238 assert_eq!(
2239 result,
2240 vec![
2241 Token::Word("echo".to_string()),
2242 Token::Word("~+".to_string())
2243 ]
2244 );
2245 }
2246
2247 #[test]
2248 fn test_tilde_expansion_oldpwd_in_quotes() {
2249 let mut shell_state = ShellState::new();
2250 shell_state.set_var("OLDPWD", "/test/old".to_string());
2251
2252 let result = lex("echo '~-'", &shell_state).unwrap();
2254 assert_eq!(
2255 result,
2256 vec![
2257 Token::Word("echo".to_string()),
2258 Token::Word("~-".to_string())
2259 ]
2260 );
2261
2262 let result = lex("echo \"~-\"", &shell_state).unwrap();
2264 assert_eq!(
2265 result,
2266 vec![
2267 Token::Word("echo".to_string()),
2268 Token::Word("~-".to_string())
2269 ]
2270 );
2271 }
2272
2273 #[test]
2274 fn test_tilde_expansion_mixed() {
2275 let _lock = ENV_LOCK.lock().unwrap();
2276 let mut shell_state = ShellState::new();
2277 let home = env::var("HOME").unwrap_or_else(|_| "/home/user".to_string());
2278 shell_state.set_var("PWD", "/current".to_string());
2279 shell_state.set_var("OLDPWD", "/previous".to_string());
2280
2281 let result = lex("echo ~ ~+ ~-", &shell_state).unwrap();
2282 assert_eq!(
2283 result,
2284 vec![
2285 Token::Word("echo".to_string()),
2286 Token::Word(home),
2287 Token::Word("/current".to_string()),
2288 Token::Word("/previous".to_string())
2289 ]
2290 );
2291 }
2292
2293 #[test]
2294 fn test_tilde_expansion_not_at_start() {
2295 let mut shell_state = ShellState::new();
2296 shell_state.set_var("PWD", "/test".to_string());
2297
2298 let result = lex("echo prefix~+", &shell_state).unwrap();
2300 assert_eq!(
2301 result,
2302 vec![
2303 Token::Word("echo".to_string()),
2304 Token::Word("prefix~+".to_string())
2305 ]
2306 );
2307 }
2308
2309 #[test]
2310 fn test_tilde_expansion_username() {
2311 let shell_state = ShellState::new();
2312
2313 let result = lex("echo ~root", &shell_state).unwrap();
2315 assert_eq!(result.len(), 2);
2316 assert_eq!(result[0], Token::Word("echo".to_string()));
2317
2318 if let Token::Word(path) = &result[1] {
2320 assert!(path == "/root" || path == "~root");
2321 } else {
2322 panic!("Expected Word token");
2323 }
2324 }
2325
2326 #[test]
2327 fn test_tilde_expansion_username_with_path() {
2328 let shell_state = ShellState::new();
2329
2330 let result = lex("echo ~root/documents", &shell_state).unwrap();
2332 assert_eq!(result.len(), 2);
2333 assert_eq!(result[0], Token::Word("echo".to_string()));
2334
2335 if let Token::Word(path) = &result[1] {
2337 assert!(path == "/root/documents" || path == "~root/documents");
2338 } else {
2339 panic!("Expected Word token");
2340 }
2341 }
2342
2343 #[test]
2344 fn test_tilde_expansion_nonexistent_user() {
2345 let shell_state = ShellState::new();
2346
2347 let result = lex("echo ~nonexistentuser12345", &shell_state).unwrap();
2349 assert_eq!(
2350 result,
2351 vec![
2352 Token::Word("echo".to_string()),
2353 Token::Word("~nonexistentuser12345".to_string())
2354 ]
2355 );
2356 }
2357
2358 #[test]
2359 fn test_tilde_expansion_username_in_quotes() {
2360 let shell_state = ShellState::new();
2361
2362 let result = lex("echo '~root'", &shell_state).unwrap();
2364 assert_eq!(
2365 result,
2366 vec![
2367 Token::Word("echo".to_string()),
2368 Token::Word("~root".to_string())
2369 ]
2370 );
2371
2372 let result = lex("echo \"~root\"", &shell_state).unwrap();
2374 assert_eq!(
2375 result,
2376 vec![
2377 Token::Word("echo".to_string()),
2378 Token::Word("~root".to_string())
2379 ]
2380 );
2381 }
2382
2383 #[test]
2384 fn test_tilde_expansion_mixed_with_username() {
2385 let _lock = ENV_LOCK.lock().unwrap();
2386 let mut shell_state = ShellState::new();
2387 let home = env::var("HOME").unwrap_or_else(|_| "/home/user".to_string());
2388 shell_state.set_var("PWD", "/current".to_string());
2389
2390 let result = lex("echo ~ ~+ ~root", &shell_state).unwrap();
2392 assert_eq!(result.len(), 4);
2393 assert_eq!(result[0], Token::Word("echo".to_string()));
2394 assert_eq!(result[1], Token::Word(home));
2395 assert_eq!(result[2], Token::Word("/current".to_string()));
2396
2397 if let Token::Word(path) = &result[3] {
2399 assert!(path == "/root" || path == "~root");
2400 } else {
2401 panic!("Expected Word token");
2402 }
2403 }
2404
2405 #[test]
2406 fn test_tilde_expansion_username_with_special_chars() {
2407 let shell_state = ShellState::new();
2408
2409 let result = lex("echo ~user@host", &shell_state).unwrap();
2411 assert_eq!(result.len(), 2);
2412 assert_eq!(result[0], Token::Word("echo".to_string()));
2413
2414 if let Token::Word(path) = &result[1] {
2416 assert!(path.contains("@host") || path == "~user@host");
2418 } else {
2419 panic!("Expected Word token");
2420 }
2421 }
2422}