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), RedirectFdIn(i32, String), RedirectFdOut(i32, String), RedirectFdAppend(i32, String), RedirectFdDup(i32, i32), RedirectFdClose(i32), RedirectFdInOut(i32, String), If,
24 Then,
25 Else,
26 Elif,
27 Fi,
28 Case,
29 In,
30 Esac,
31 DoubleSemicolon,
32 Semicolon,
33 RightParen,
34 LeftParen,
35 LeftBrace,
36 RightBrace,
37 Newline,
38 Local,
39 Return,
40 For,
41 Do,
42 Done,
43 While, Until, Break, Continue, And, Or, }
50
51fn is_keyword(word: &str) -> Option<Token> {
52 match word {
53 "if" => Some(Token::If),
54 "then" => Some(Token::Then),
55 "else" => Some(Token::Else),
56 "elif" => Some(Token::Elif),
57 "fi" => Some(Token::Fi),
58 "case" => Some(Token::Case),
59 "in" => Some(Token::In),
60 "esac" => Some(Token::Esac),
61 "local" => Some(Token::Local),
62 "return" => Some(Token::Return),
63 "for" => Some(Token::For),
64 "while" => Some(Token::While),
65 "until" => Some(Token::Until),
66 "break" => Some(Token::Break),
67 "continue" => Some(Token::Continue),
68 "do" => Some(Token::Do),
69 "done" => Some(Token::Done),
70 _ => None,
71 }
72}
73
74pub fn is_shell_keyword(word: &str) -> bool {
77 if is_keyword(word).is_some() {
79 return true;
80 }
81
82 matches!(word, "until" | "{" | "}" | "!")
85}
86
87fn skip_whitespace(chars: &mut std::iter::Peekable<std::str::Chars>) {
89 while let Some(&ch) = chars.peek() {
90 if ch == ' ' || ch == '\t' {
91 chars.next();
92 } else {
93 break;
94 }
95 }
96}
97
98fn flush_current_token(current: &mut String, tokens: &mut Vec<Token>, was_quoted: bool) {
100 if !current.is_empty() {
101 if !was_quoted {
104 if let Some(keyword) = is_keyword(current) {
105 tokens.push(keyword);
106 current.clear();
107 return;
108 }
109 }
110 tokens.push(Token::Word(current.clone()));
111 current.clear();
112 }
113}
114
115fn collect_until_closing_brace(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
118 let mut content = String::new();
119
120 while let Some(&ch) = chars.peek() {
121 if ch == '}' {
122 chars.next(); break;
124 } else {
125 content.push(ch);
126 chars.next();
127 }
128 }
129
130 content
131}
132
133fn collect_with_paren_depth(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
138 let mut content = String::new();
139 let mut paren_depth = 1; let mut in_single_quote = false;
141 let mut in_double_quote = false;
142
143 while let Some(&ch) = chars.peek() {
144 if ch == '\'' && !in_double_quote {
145 in_single_quote = !in_single_quote;
147 content.push(ch);
148 chars.next();
149 } else if ch == '"' && !in_single_quote {
150 in_double_quote = !in_double_quote;
152 content.push(ch);
153 chars.next();
154 } else if ch == '(' && !in_single_quote && !in_double_quote {
155 paren_depth += 1;
156 content.push(ch);
157 chars.next();
158 } else if ch == ')' && !in_single_quote && !in_double_quote {
159 paren_depth -= 1;
160 if paren_depth == 0 {
161 chars.next(); break;
163 } else {
164 content.push(ch);
165 chars.next();
166 }
167 } else {
168 content.push(ch);
169 chars.next();
170 }
171 }
172
173 content
174}
175
176fn parse_variable_name(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
181 let mut var_name = String::new();
182
183 if let Some(&ch) = chars.peek() {
185 if ch == '?'
186 || ch == '$'
187 || ch == '0'
188 || ch == '#'
189 || ch == '@'
190 || ch == '*'
191 || ch == '!'
192 || ch.is_ascii_digit()
193 {
194 var_name.push(ch);
195 chars.next();
196 } else {
197 while let Some(&ch) = chars.peek() {
200 if ch.is_alphanumeric() || ch == '_' {
201 var_name.push(ch);
202 chars.next();
203 } else {
204 break;
205 }
206 }
207 }
208 }
209
210 var_name
211}
212
213fn expand_variables_in_command(command: &str, shell_state: &ShellState) -> String {
214 if command.contains("$(") || command.contains('`') {
216 return command.to_string();
217 }
218
219 let mut chars = command.chars().peekable();
220 let mut current = String::new();
221
222 while let Some(&ch) = chars.peek() {
223 if ch == '$' {
224 chars.next(); if let Some(&'{') = chars.peek() {
226 chars.next(); let param_content = collect_until_closing_brace(&mut chars);
229
230 if !param_content.is_empty() {
231 if param_content.starts_with('#') && param_content.len() > 1 {
233 let var_name = ¶m_content[1..];
234 if let Some(val) = shell_state.get_var(var_name) {
235 current.push_str(&val.len().to_string());
236 } else {
237 current.push('0');
238 }
239 } else {
240 match parse_parameter_expansion(¶m_content) {
242 Ok(expansion) => {
243 match expand_parameter(&expansion, shell_state) {
244 Ok(expanded) => {
245 current.push_str(&expanded);
246 }
247 Err(_) => {
248 current.push_str("${");
250 current.push_str(¶m_content);
251 current.push('}');
252 }
253 }
254 }
255 Err(_) => {
256 current.push_str("${");
258 current.push_str(¶m_content);
259 current.push('}');
260 }
261 }
262 }
263 } else {
264 current.push_str("${}");
266 }
267 } else if let Some(&'(') = chars.peek() {
268 current.push('$');
270 current.push('(');
271 chars.next();
272 } else if let Some(&'`') = chars.peek() {
273 current.push('$');
275 current.push('`');
276 chars.next();
277 } else {
278 let var_name = parse_variable_name(&mut chars);
280
281 if !var_name.is_empty() {
282 if let Some(val) = shell_state.get_var(&var_name) {
283 current.push_str(&val);
284 } else {
285 current.push('$');
286 current.push_str(&var_name);
287 }
288 } else {
289 current.push('$');
290 }
291 }
292 } else if ch == '`' {
293 current.push(ch);
295 chars.next();
296 } else {
297 current.push(ch);
298 chars.next();
299 }
300 }
301
302 if current.contains('$') {
304 let mut final_result = String::new();
306 let mut chars = current.chars().peekable();
307
308 while let Some(&ch) = chars.peek() {
309 if ch == '$' {
310 chars.next(); if let Some(&'{') = chars.peek() {
312 chars.next(); let param_content = collect_until_closing_brace(&mut chars);
315
316 if !param_content.is_empty() {
317 if param_content.starts_with('#') && param_content.len() > 1 {
319 let var_name = ¶m_content[1..];
320 if let Some(val) = shell_state.get_var(var_name) {
321 final_result.push_str(&val.len().to_string());
322 } else {
323 final_result.push('0');
324 }
325 } else {
326 match parse_parameter_expansion(¶m_content) {
328 Ok(expansion) => {
329 match expand_parameter(&expansion, shell_state) {
330 Ok(expanded) => {
331 if expanded.is_empty() {
332 } else {
336 final_result.push_str(&expanded);
337 }
338 }
339 Err(_) => {
340 final_result.push_str("${");
342 final_result.push_str(¶m_content);
343 final_result.push('}');
344 }
345 }
346 }
347 Err(_) => {
348 final_result.push_str("${");
350 final_result.push_str(¶m_content);
351 final_result.push('}');
352 }
353 }
354 }
355 } else {
356 final_result.push_str("${}");
358 }
359 } else {
360 let var_name = parse_variable_name(&mut chars);
361
362 if !var_name.is_empty() {
363 if let Some(val) = shell_state.get_var(&var_name) {
364 final_result.push_str(&val);
365 } else {
366 final_result.push('$');
367 final_result.push_str(&var_name);
368 }
369 } else {
370 final_result.push('$');
371 }
372 }
373 } else {
374 final_result.push(ch);
375 chars.next();
376 }
377 }
378 final_result
379 } else {
380 current
381 }
382}
383
384pub fn lex(input: &str, shell_state: &ShellState) -> Result<Vec<Token>, String> {
385 let mut tokens = Vec::new();
386 let mut chars = input.chars().peekable();
387 let mut current = String::new();
388 let mut in_double_quote = false;
389 let mut in_single_quote = false;
390 let mut just_closed_quote = false; let mut was_quoted = false; while let Some(&ch) = chars.peek() {
394 match ch {
395 ' ' | '\t' if !in_double_quote && !in_single_quote => {
396 if just_closed_quote && current.is_empty() {
398 tokens.push(Token::Word("".to_string()));
399 just_closed_quote = false;
400 was_quoted = false; } else {
402 flush_current_token(&mut current, &mut tokens, was_quoted);
403 was_quoted = false; }
405 chars.next();
406 }
407 '\n' if !in_double_quote && !in_single_quote => {
408 if just_closed_quote && current.is_empty() {
410 tokens.push(Token::Word("".to_string()));
411 just_closed_quote = false;
412 was_quoted = false; } else {
414 flush_current_token(&mut current, &mut tokens, was_quoted);
415 was_quoted = false; }
417 tokens.push(Token::Newline);
418 chars.next();
419 }
420 '"' if !in_single_quote => {
421 let is_escaped = current.ends_with('\\');
423
424 if is_escaped && in_double_quote {
425 current.pop(); current.push('"'); chars.next(); just_closed_quote = false;
430 } else {
431 chars.next(); if in_double_quote {
433 just_closed_quote = current.is_empty();
438 in_double_quote = false;
439 was_quoted = true; } else {
441 in_double_quote = true;
444 just_closed_quote = false;
445 }
446 }
447 }
448 '\\' if in_double_quote => {
449 chars.next(); if let Some(&next_ch) = chars.peek() {
452 if next_ch == '$'
454 || next_ch == '`'
455 || next_ch == '"'
456 || next_ch == '\\'
457 || next_ch == '\n'
458 {
459 current.push(next_ch);
461 chars.next(); } else {
463 current.push('\\');
465 current.push(next_ch);
466 chars.next();
467 }
468 } else {
469 current.push('\\');
471 }
472 }
473 '\'' => {
474 if in_single_quote {
475 just_closed_quote = current.is_empty();
479 in_single_quote = false;
480 was_quoted = true; } else if !in_double_quote {
482 in_single_quote = true;
485 just_closed_quote = false;
486 }
487 chars.next();
488 }
489 '$' if !in_single_quote => {
490 just_closed_quote = false; chars.next(); if let Some(&'{') = chars.peek() {
493 chars.next(); let param_content = collect_until_closing_brace(&mut chars);
496
497 if !param_content.is_empty() {
498 if param_content.starts_with('#') && param_content.len() > 1 {
500 let var_name = ¶m_content[1..];
501 if let Some(val) = shell_state.get_var(var_name) {
502 current.push_str(&val.len().to_string());
503 } else {
504 current.push('0');
505 }
506 } else {
507 match parse_parameter_expansion(¶m_content) {
509 Ok(expansion) => {
510 match expand_parameter(&expansion, shell_state) {
511 Ok(expanded) => {
512 if expanded.is_empty() {
513 if !in_double_quote && !in_single_quote {
516 if !current.is_empty() {
518 if let Some(keyword) = is_keyword(¤t)
519 {
520 tokens.push(keyword);
521 } else {
522 let word = expand_variables_in_command(
523 ¤t,
524 shell_state,
525 );
526 tokens.push(Token::Word(word));
527 }
528 current.clear();
529 }
530 tokens.push(Token::Word("".to_string()));
532 }
533 } else {
535 current.push_str(&expanded);
536 }
537 }
538 Err(_) => {
539 if !current.is_empty() {
541 if let Some(keyword) = is_keyword(¤t) {
542 tokens.push(keyword);
543 } else {
544 let word = expand_variables_in_command(
545 ¤t,
546 shell_state,
547 );
548 tokens.push(Token::Word(word));
549 }
550 current.clear();
551 }
552 if let Some(space_pos) = param_content.find(' ') {
554 let first_part =
556 format!("${{{}}}", ¶m_content[..space_pos]);
557 let second_part = format!(
558 "{}}}",
559 ¶m_content[space_pos + 1..]
560 );
561 tokens.push(Token::Word(first_part));
562 tokens.push(Token::Word(second_part));
563 } else {
564 let literal = format!("${{{}}}", param_content);
565 tokens.push(Token::Word(literal));
566 }
567 }
568 }
569 }
570 Err(_) => {
571 current.push_str("${");
573 current.push_str(¶m_content);
574 current.push('}');
575 }
576 }
577 }
578 } else {
579 current.push_str("${}");
581 }
582 } else if let Some(&'(') = chars.peek() {
583 chars.next(); if let Some(&'(') = chars.peek() {
585 chars.next(); let arithmetic_expr = collect_with_paren_depth(&mut chars);
588 let found_closing = if let Some(&')') = chars.peek() {
590 chars.next(); true
592 } else {
593 false
594 };
595 current.push_str("$((");
597 current.push_str(&arithmetic_expr);
598 if found_closing {
599 current.push_str("))");
600 }
601 } else {
602 let sub_command = collect_with_paren_depth(&mut chars);
605 current.push_str("$(");
607 current.push_str(&sub_command);
608 current.push(')');
609 }
610 } else {
611 let var_name = parse_variable_name(&mut chars);
613
614 if !var_name.is_empty() {
615 current.push('$');
617 current.push_str(&var_name);
618 } else {
619 current.push('$');
620 }
621 }
622 }
623 '|' if !in_double_quote && !in_single_quote => {
624 flush_current_token(&mut current, &mut tokens, false);
625 chars.next(); if let Some(&'|') = chars.peek() {
628 chars.next(); tokens.push(Token::Or);
630 } else {
631 tokens.push(Token::Pipe);
632 }
633 skip_whitespace(&mut chars);
635 }
636 '&' if !in_double_quote && !in_single_quote => {
637 flush_current_token(&mut current, &mut tokens, false);
638 chars.next(); if let Some(&'&') = chars.peek() {
641 chars.next(); tokens.push(Token::And);
643 skip_whitespace(&mut chars);
645 } else {
646 current.push('&');
648 }
649 }
650 '>' if !in_double_quote && !in_single_quote => {
651 let fd_num = if !current.is_empty() {
654 if let Some(last_char) = current.chars().last() {
655 if last_char.is_ascii_digit() {
656 let fd = last_char.to_digit(10).unwrap() as i32;
658 current.pop();
660 Some(fd)
661 } else {
662 None
663 }
664 } else {
665 None
666 }
667 } else {
668 None
669 };
670
671 flush_current_token(&mut current, &mut tokens, false);
673
674 chars.next(); if let Some(&'&') = chars.peek() {
678 chars.next(); let mut target = String::new();
682 while let Some(&ch) = chars.peek() {
683 if ch.is_ascii_digit() || ch == '-' {
684 target.push(ch);
685 chars.next();
686 } else {
687 break;
688 }
689 }
690
691 if !target.is_empty() {
692 let source_fd = fd_num.unwrap_or(1); if target == "-" {
695 tokens.push(Token::RedirectFdClose(source_fd));
697 } else if let Ok(target_fd) = target.parse::<i32>() {
698 tokens.push(Token::RedirectFdDup(source_fd, target_fd));
700 } else {
701 return Err(format!("Invalid file descriptor: {}", target));
703 }
704 skip_whitespace(&mut chars);
705 } else {
706 return Err(
708 "Invalid redirection syntax: expected fd number or '-' after >&"
709 .to_string(),
710 );
711 }
712 } else if let Some(&'>') = chars.peek() {
713 chars.next(); skip_whitespace(&mut chars);
716
717 let mut filename = String::new();
719 let mut in_filename_quote = false;
720 let mut filename_quote_char = ' ';
721
722 while let Some(&ch) = chars.peek() {
723 if !in_filename_quote && (ch == '"' || ch == '\'') {
724 in_filename_quote = true;
725 filename_quote_char = ch;
726 chars.next(); } else if in_filename_quote && ch == filename_quote_char {
728 in_filename_quote = false;
729 chars.next(); } else if !in_filename_quote && (ch == ' ' || ch == '\t' || ch == '\n'
731 || ch == ';' || ch == '|' || ch == '&' || ch == '>' || ch == '<')
732 {
733 break;
734 } else {
735 filename.push(ch);
736 chars.next();
737 }
738 }
739
740 if !filename.is_empty() {
741 if let Some(fd) = fd_num {
742 tokens.push(Token::RedirectFdAppend(fd, filename));
743 } else {
744 tokens.push(Token::RedirAppend);
745 tokens.push(Token::Word(filename));
746 }
747 } else {
748 if fd_num.is_some() {
750 return Err(
751 "Invalid redirection: expected filename after >>".to_string()
752 );
753 } else {
754 tokens.push(Token::RedirAppend);
755 }
756 }
757 } else {
758 skip_whitespace(&mut chars);
760
761 let mut filename = String::new();
763 let mut in_filename_quote = false;
764 let mut filename_quote_char = ' ';
765
766 while let Some(&ch) = chars.peek() {
767 if !in_filename_quote && (ch == '"' || ch == '\'') {
768 in_filename_quote = true;
769 filename_quote_char = ch;
770 chars.next(); } else if in_filename_quote && ch == filename_quote_char {
772 in_filename_quote = false;
773 chars.next(); } else if !in_filename_quote && (ch == ' ' || ch == '\t' || ch == '\n'
775 || ch == ';' || ch == '|' || ch == '&' || ch == '>' || ch == '<')
776 {
777 break;
778 } else {
779 filename.push(ch);
780 chars.next();
781 }
782 }
783
784 if !filename.is_empty() {
785 if let Some(fd) = fd_num {
786 tokens.push(Token::RedirectFdOut(fd, filename));
787 } else {
788 tokens.push(Token::RedirOut);
789 tokens.push(Token::Word(filename));
790 }
791 } else {
792 if fd_num.is_some() {
794 return Err(
795 "Invalid redirection: expected filename after >".to_string()
796 );
797 } else {
798 tokens.push(Token::RedirOut);
799 }
800 }
801 }
802 }
803 '<' if !in_double_quote && !in_single_quote => {
804 let fd_num = if !current.is_empty() {
806 if let Some(last_char) = current.chars().last() {
807 if last_char.is_ascii_digit() {
808 let fd = last_char.to_digit(10).unwrap() as i32;
810 current.pop();
812 Some(fd)
813 } else {
814 None
815 }
816 } else {
817 None
818 }
819 } else {
820 None
821 };
822
823 flush_current_token(&mut current, &mut tokens, false);
825
826 chars.next(); if let Some(&'&') = chars.peek() {
830 chars.next(); let mut target = String::new();
834 while let Some(&ch) = chars.peek() {
835 if ch.is_ascii_digit() || ch == '-' {
836 target.push(ch);
837 chars.next();
838 } else {
839 break;
840 }
841 }
842
843 if !target.is_empty() {
844 let source_fd = fd_num.unwrap_or(0); if target == "-" {
847 tokens.push(Token::RedirectFdClose(source_fd));
849 } else if let Ok(target_fd) = target.parse::<i32>() {
850 tokens.push(Token::RedirectFdDup(source_fd, target_fd));
852 } else {
853 return Err(format!("Invalid file descriptor: {}", target));
855 }
856 skip_whitespace(&mut chars);
857 } else {
858 return Err(
860 "Invalid redirection syntax: expected fd number or '-' after <&"
861 .to_string(),
862 );
863 }
864 } else if let Some(&'<') = chars.peek() {
865 chars.next(); if let Some(&'<') = chars.peek() {
868 chars.next(); skip_whitespace(&mut chars);
871
872 let mut content = String::new();
873 let mut in_quote = false;
874 let mut quote_char = ' ';
875
876 while let Some(&ch) = chars.peek() {
877 if ch == '\n' && !in_quote {
878 break;
879 }
880 if (ch == '"' || ch == '\'') && !in_quote {
881 in_quote = true;
882 quote_char = ch;
883 chars.next(); } else if in_quote && ch == quote_char {
885 in_quote = false;
886 chars.next(); } else if !in_quote && (ch == ' ' || ch == '\t') {
888 break;
889 } else {
890 content.push(ch);
891 chars.next();
892 }
893 }
894
895 if !content.is_empty() {
896 tokens.push(Token::RedirHereString(content));
897 } else {
898 return Err("Invalid here-string syntax: expected content after <<<"
899 .to_string());
900 }
901 } else {
902 skip_whitespace(&mut chars);
904
905 let mut delimiter = String::new();
906 let mut in_quote = false;
907 let mut quote_char = ' ';
908 let mut was_quoted = false; while let Some(&ch) = chars.peek() {
911 if ch == '\n' && !in_quote {
912 break;
913 }
914 if (ch == '"' || ch == '\'') && !in_quote {
915 in_quote = true;
916 quote_char = ch;
917 was_quoted = true; chars.next(); } else if in_quote && ch == quote_char {
920 in_quote = false;
921 chars.next(); } else if !in_quote && (ch == ' ' || ch == '\t') {
923 break;
924 } else {
925 delimiter.push(ch);
926 chars.next();
927 }
928 }
929
930 if !delimiter.is_empty() {
931 tokens.push(Token::RedirHereDoc(delimiter, was_quoted));
933 } else {
934 return Err(
935 "Invalid here-document syntax: expected delimiter after <<"
936 .to_string(),
937 );
938 }
939 }
940 } else if let Some(&'>') = chars.peek() {
941 chars.next(); skip_whitespace(&mut chars);
944
945 let mut filename = String::new();
947 let mut in_filename_quote = false;
948 let mut filename_quote_char = ' ';
949
950 while let Some(&ch) = chars.peek() {
951 if !in_filename_quote && (ch == '"' || ch == '\'') {
952 in_filename_quote = true;
953 filename_quote_char = ch;
954 chars.next(); } else if in_filename_quote && ch == filename_quote_char {
956 in_filename_quote = false;
957 chars.next(); } else if !in_filename_quote && (ch == ' ' || ch == '\t' || ch == '\n'
959 || ch == ';' || ch == '|' || ch == '&' || ch == '>' || ch == '<')
960 {
961 break;
962 } else {
963 filename.push(ch);
964 chars.next();
965 }
966 }
967
968 if !filename.is_empty() {
969 let fd = fd_num.unwrap_or(0); tokens.push(Token::RedirectFdInOut(fd, filename));
971 } else {
972 return Err("Invalid redirection: expected filename after <>".to_string());
973 }
974 } else {
975 skip_whitespace(&mut chars);
977
978 let mut filename = String::new();
980 let mut in_filename_quote = false;
981 let mut filename_quote_char = ' ';
982
983 while let Some(&ch) = chars.peek() {
984 if !in_filename_quote && (ch == '"' || ch == '\'') {
985 in_filename_quote = true;
986 filename_quote_char = ch;
987 chars.next(); } else if in_filename_quote && ch == filename_quote_char {
989 in_filename_quote = false;
990 chars.next(); } else if !in_filename_quote && (ch == ' ' || ch == '\t' || ch == '\n'
992 || ch == ';' || ch == '|' || ch == '&' || ch == '>' || ch == '<')
993 {
994 break;
995 } else {
996 filename.push(ch);
997 chars.next();
998 }
999 }
1000
1001 if !filename.is_empty() {
1002 if let Some(fd) = fd_num {
1003 tokens.push(Token::RedirectFdIn(fd, filename));
1004 } else {
1005 tokens.push(Token::RedirIn);
1006 tokens.push(Token::Word(filename));
1007 }
1008 } else {
1009 if fd_num.is_some() {
1011 return Err(
1012 "Invalid redirection: expected filename after <".to_string()
1013 );
1014 } else {
1015 tokens.push(Token::RedirIn);
1016 }
1017 }
1018 }
1019 }
1020 ')' if !in_double_quote && !in_single_quote => {
1021 flush_current_token(&mut current, &mut tokens, false);
1022 tokens.push(Token::RightParen);
1023 chars.next();
1024 }
1025 '}' if !in_double_quote && !in_single_quote => {
1026 flush_current_token(&mut current, &mut tokens, false);
1027 tokens.push(Token::RightBrace);
1028 chars.next();
1029 }
1030 '(' if !in_double_quote && !in_single_quote => {
1031 flush_current_token(&mut current, &mut tokens, false);
1032 tokens.push(Token::LeftParen);
1033 chars.next();
1034 }
1035 '{' if !in_double_quote && !in_single_quote => {
1036 let mut temp_chars = chars.clone();
1038 let mut brace_content = String::new();
1039 let mut depth = 1;
1040 let mut temp_in_single_quote = false;
1041 let mut temp_in_double_quote = false;
1042
1043 temp_chars.next(); while let Some(&ch) = temp_chars.peek() {
1046 if ch == '\'' && !temp_in_double_quote {
1047 temp_in_single_quote = !temp_in_single_quote;
1048 } else if ch == '"' && !temp_in_single_quote {
1049 temp_in_double_quote = !temp_in_double_quote;
1050 } else if !temp_in_single_quote && !temp_in_double_quote {
1051 if ch == '{' {
1052 depth += 1;
1053 } else if ch == '}' {
1054 depth -= 1;
1055 if depth == 0 {
1056 break;
1057 }
1058 }
1059 }
1060 brace_content.push(ch);
1061 temp_chars.next();
1062 }
1063
1064 if depth == 0 && !brace_content.trim().is_empty() {
1065 let mut has_brace_expansion_pattern = false;
1067 let mut check_chars = brace_content.chars().peekable();
1068 let mut check_in_single = false;
1069 let mut check_in_double = false;
1070
1071 while let Some(ch) = check_chars.next() {
1072 if ch == '\'' && !check_in_double {
1073 check_in_single = !check_in_single;
1074 } else if ch == '"' && !check_in_single {
1075 check_in_double = !check_in_double;
1076 } else if !check_in_single && !check_in_double {
1077 if ch == ',' {
1078 has_brace_expansion_pattern = true;
1079 break;
1080 } else if ch == '.' && check_chars.peek() == Some(&'.') {
1081 has_brace_expansion_pattern = true;
1082 break;
1083 }
1084 }
1085 }
1086
1087 if has_brace_expansion_pattern {
1088 current.push('{');
1090 current.push_str(&brace_content);
1091 current.push('}');
1092 chars.next(); let mut content_depth = 1;
1095 while let Some(&ch) = chars.peek() {
1096 chars.next();
1097 if ch == '{' {
1098 content_depth += 1;
1099 } else if ch == '}' {
1100 content_depth -= 1;
1101 if content_depth == 0 {
1102 break;
1103 }
1104 }
1105 }
1106 } else {
1107 flush_current_token(&mut current, &mut tokens, false);
1109 tokens.push(Token::LeftBrace);
1110 chars.next();
1111 }
1112 } else {
1113 flush_current_token(&mut current, &mut tokens, false);
1115 tokens.push(Token::LeftBrace);
1116 chars.next();
1117 }
1118 }
1119 '`' => {
1120 flush_current_token(&mut current, &mut tokens, false);
1121 chars.next();
1122 let mut sub_command = String::new();
1123 while let Some(&ch) = chars.peek() {
1124 if ch == '`' {
1125 chars.next();
1126 break;
1127 } else {
1128 sub_command.push(ch);
1129 chars.next();
1130 }
1131 }
1132 current.push('`');
1134 current.push_str(&sub_command);
1135 current.push('`');
1136 }
1137 ';' if !in_double_quote && !in_single_quote => {
1138 if just_closed_quote && current.is_empty() {
1140 tokens.push(Token::Word("".to_string()));
1141 just_closed_quote = false;
1142 was_quoted = false; } else {
1144 flush_current_token(&mut current, &mut tokens, false);
1145 }
1146 chars.next();
1147 if let Some(&next_ch) = chars.peek() {
1148 if next_ch == ';' {
1149 chars.next();
1150 tokens.push(Token::DoubleSemicolon);
1151 } else {
1152 tokens.push(Token::Semicolon);
1153 }
1154 } else {
1155 tokens.push(Token::Semicolon);
1156 }
1157 }
1158 _ => {
1159 if ch == '~' && current.is_empty() && !in_single_quote && !in_double_quote {
1163 chars.next(); if let Some(&next_ch) = chars.peek() {
1167 if next_ch == '+' {
1168 chars.next(); if let Some(pwd) =
1171 shell_state.get_var("PWD").or_else(|| env::var("PWD").ok())
1172 {
1173 current.push_str(&pwd);
1174 } else if let Ok(pwd) = env::current_dir() {
1175 current.push_str(&pwd.to_string_lossy());
1176 } else {
1177 current.push_str("~+");
1178 }
1179 } else if next_ch == '-' {
1180 chars.next(); if let Some(oldpwd) = shell_state
1183 .get_var("OLDPWD")
1184 .or_else(|| env::var("OLDPWD").ok())
1185 {
1186 current.push_str(&oldpwd);
1187 } else {
1188 current.push_str("~-");
1189 }
1190 } else if next_ch == '/'
1191 || next_ch == ' '
1192 || next_ch == '\t'
1193 || next_ch == '\n'
1194 {
1195 if let Ok(home) = env::var("HOME") {
1197 current.push_str(&home);
1198 } else {
1199 current.push('~');
1200 }
1201 } else {
1202 let mut username = String::new();
1204 while let Some(&ch) = chars.peek() {
1205 if ch == '/' || ch == ' ' || ch == '\t' || ch == '\n' {
1206 break;
1207 }
1208 username.push(ch);
1209 chars.next();
1210 }
1211
1212 if !username.is_empty() {
1213 let user_home = if username == "root" {
1216 "/root".to_string()
1217 } else {
1218 format!("/home/{}", username)
1219 };
1220
1221 if std::path::Path::new(&user_home).exists() {
1223 current.push_str(&user_home);
1224 } else {
1225 current.push('~');
1227 current.push_str(&username);
1228 }
1229 } else {
1230 if let Ok(home) = env::var("HOME") {
1232 current.push_str(&home);
1233 } else {
1234 current.push('~');
1235 }
1236 }
1237 }
1238 } else {
1239 if let Ok(home) = env::var("HOME") {
1241 current.push_str(&home);
1242 } else {
1243 current.push('~');
1244 }
1245 }
1246 } else {
1247 just_closed_quote = false; current.push(ch);
1249 chars.next();
1250 }
1251 }
1252 }
1253 }
1254
1255 if just_closed_quote && current.is_empty() {
1257 tokens.push(Token::Word("".to_string()));
1258 } else {
1259 flush_current_token(&mut current, &mut tokens, was_quoted);
1260 }
1261
1262 Ok(tokens)
1263}
1264
1265pub fn expand_aliases(
1267 tokens: Vec<Token>,
1268 shell_state: &ShellState,
1269 expanded: &mut HashSet<String>,
1270) -> Result<Vec<Token>, String> {
1271 if tokens.is_empty() {
1272 return Ok(tokens);
1273 }
1274
1275 if let Token::Word(ref word) = tokens[0] {
1277 if let Some(alias_value) = shell_state.get_alias(word) {
1278 if expanded.contains(word) {
1280 return Err(format!("Alias '{}' recursion detected", word));
1281 }
1282
1283 expanded.insert(word.clone());
1285
1286 let alias_tokens = lex(alias_value, shell_state)?;
1288
1289 let expanded_alias_tokens = if !alias_tokens.is_empty() {
1297 if let Token::Word(ref first_word) = alias_tokens[0] {
1298 if first_word != word
1300 && shell_state.get_alias(first_word).is_some()
1301 && !expanded.contains(first_word)
1302 {
1303 expand_aliases(alias_tokens, shell_state, expanded)?
1304 } else {
1305 alias_tokens
1306 }
1307 } else {
1308 alias_tokens
1309 }
1310 } else {
1311 alias_tokens
1312 };
1313
1314 expanded.remove(word);
1316
1317 let mut result = expanded_alias_tokens;
1319 result.extend_from_slice(&tokens[1..]);
1320 Ok(result)
1321 } else {
1322 Ok(tokens)
1324 }
1325 } else {
1326 Ok(tokens)
1328 }
1329}
1330
1331#[cfg(test)]
1332mod tests {
1333 use super::*;
1334 use std::sync::Mutex;
1335
1336 static ENV_LOCK: Mutex<()> = Mutex::new(());
1338
1339 fn expand_tokens(tokens: Vec<Token>, shell_state: &mut ShellState) -> Vec<Token> {
1342 let mut result = Vec::new();
1343 for token in tokens {
1344 match token {
1345 Token::Word(word) => {
1346 let expanded = crate::executor::expand_variables_in_string(&word, shell_state);
1348 if !expanded.is_empty() || !word.starts_with("$(") {
1351 result.push(Token::Word(expanded));
1352 }
1353 }
1354 other => result.push(other),
1355 }
1356 }
1357 result
1358 }
1359
1360 #[test]
1361 fn test_basic_word() {
1362 let shell_state = ShellState::new();
1363 let result = lex("ls", &shell_state).unwrap();
1364 assert_eq!(result, vec![Token::Word("ls".to_string())]);
1365 }
1366
1367 #[test]
1368 fn test_multiple_words() {
1369 let shell_state = ShellState::new();
1370 let result = lex("ls -la", &shell_state).unwrap();
1371 assert_eq!(
1372 result,
1373 vec![
1374 Token::Word("ls".to_string()),
1375 Token::Word("-la".to_string())
1376 ]
1377 );
1378 }
1379
1380 #[test]
1381 fn test_pipe() {
1382 let shell_state = ShellState::new();
1383 let result = lex("ls | grep txt", &shell_state).unwrap();
1384 assert_eq!(
1385 result,
1386 vec![
1387 Token::Word("ls".to_string()),
1388 Token::Pipe,
1389 Token::Word("grep".to_string()),
1390 Token::Word("txt".to_string())
1391 ]
1392 );
1393 }
1394
1395 #[test]
1396 fn test_redirections() {
1397 let shell_state = ShellState::new();
1398 let result = lex("printf hello > output.txt", &shell_state).unwrap();
1399 assert_eq!(
1400 result,
1401 vec![
1402 Token::Word("printf".to_string()),
1403 Token::Word("hello".to_string()),
1404 Token::RedirOut,
1405 Token::Word("output.txt".to_string())
1406 ]
1407 );
1408 }
1409
1410 #[test]
1411 fn test_append_redirection() {
1412 let shell_state = ShellState::new();
1413 let result = lex("printf hello >> output.txt", &shell_state).unwrap();
1414 assert_eq!(
1415 result,
1416 vec![
1417 Token::Word("printf".to_string()),
1418 Token::Word("hello".to_string()),
1419 Token::RedirAppend,
1420 Token::Word("output.txt".to_string())
1421 ]
1422 );
1423 }
1424
1425 #[test]
1426 fn test_input_redirection() {
1427 let shell_state = ShellState::new();
1428 let result = lex("cat < input.txt", &shell_state).unwrap();
1429 assert_eq!(
1430 result,
1431 vec![
1432 Token::Word("cat".to_string()),
1433 Token::RedirIn,
1434 Token::Word("input.txt".to_string())
1435 ]
1436 );
1437 }
1438
1439 #[test]
1440 fn test_double_quotes() {
1441 let shell_state = ShellState::new();
1442 let result = lex("echo \"hello world\"", &shell_state).unwrap();
1443 assert_eq!(
1444 result,
1445 vec![
1446 Token::Word("echo".to_string()),
1447 Token::Word("hello world".to_string())
1448 ]
1449 );
1450 }
1451
1452 #[test]
1453 fn test_single_quotes() {
1454 let shell_state = ShellState::new();
1455 let result = lex("echo 'hello world'", &shell_state).unwrap();
1456 assert_eq!(
1457 result,
1458 vec![
1459 Token::Word("echo".to_string()),
1460 Token::Word("hello world".to_string())
1461 ]
1462 );
1463 }
1464
1465 #[test]
1466 fn test_variable_expansion() {
1467 let mut shell_state = ShellState::new();
1468 shell_state.set_var("TEST_VAR", "expanded_value".to_string());
1469 let tokens = lex("echo $TEST_VAR", &shell_state).unwrap();
1470 let result = expand_tokens(tokens, &mut shell_state);
1471 assert_eq!(
1472 result,
1473 vec![
1474 Token::Word("echo".to_string()),
1475 Token::Word("expanded_value".to_string())
1476 ]
1477 );
1478 }
1479
1480 #[test]
1481 fn test_variable_expansion_nonexistent() {
1482 let shell_state = ShellState::new();
1483 let result = lex("echo $TEST_VAR2", &shell_state).unwrap();
1484 assert_eq!(
1485 result,
1486 vec![
1487 Token::Word("echo".to_string()),
1488 Token::Word("$TEST_VAR2".to_string())
1489 ]
1490 );
1491 }
1492
1493 #[test]
1494 fn test_empty_variable() {
1495 let shell_state = ShellState::new();
1496 let result = lex("echo $", &shell_state).unwrap();
1497 assert_eq!(
1498 result,
1499 vec![
1500 Token::Word("echo".to_string()),
1501 Token::Word("$".to_string())
1502 ]
1503 );
1504 }
1505
1506 #[test]
1507 fn test_mixed_quotes_and_variables() {
1508 let mut shell_state = ShellState::new();
1509 shell_state.set_var("USER", "alice".to_string());
1510 let tokens = lex("echo \"Hello $USER\"", &shell_state).unwrap();
1511 let result = expand_tokens(tokens, &mut shell_state);
1512 assert_eq!(
1513 result,
1514 vec![
1515 Token::Word("echo".to_string()),
1516 Token::Word("Hello alice".to_string())
1517 ]
1518 );
1519 }
1520
1521 #[test]
1522 fn test_unclosed_double_quote() {
1523 let shell_state = ShellState::new();
1525 let result = lex("echo \"hello", &shell_state).unwrap();
1526 assert_eq!(
1527 result,
1528 vec![
1529 Token::Word("echo".to_string()),
1530 Token::Word("hello".to_string())
1531 ]
1532 );
1533 }
1534
1535 #[test]
1536 fn test_empty_input() {
1537 let shell_state = ShellState::new();
1538 let result = lex("", &shell_state).unwrap();
1539 assert_eq!(result, Vec::<Token>::new());
1540 }
1541
1542 #[test]
1543 fn test_only_spaces() {
1544 let shell_state = ShellState::new();
1545 let result = lex(" ", &shell_state).unwrap();
1546 assert_eq!(result, Vec::<Token>::new());
1547 }
1548
1549 #[test]
1550 fn test_complex_pipeline() {
1551 let shell_state = ShellState::new();
1552 let result = lex(
1553 "cat input.txt | grep \"search term\" > output.txt",
1554 &shell_state,
1555 )
1556 .unwrap();
1557 assert_eq!(
1558 result,
1559 vec![
1560 Token::Word("cat".to_string()),
1561 Token::Word("input.txt".to_string()),
1562 Token::Pipe,
1563 Token::Word("grep".to_string()),
1564 Token::Word("search term".to_string()),
1565 Token::RedirOut,
1566 Token::Word("output.txt".to_string())
1567 ]
1568 );
1569 }
1570
1571 #[test]
1572 fn test_if_tokens() {
1573 let shell_state = ShellState::new();
1574 let result = lex("if true; then printf yes; fi", &shell_state).unwrap();
1575 assert_eq!(
1576 result,
1577 vec![
1578 Token::If,
1579 Token::Word("true".to_string()),
1580 Token::Semicolon,
1581 Token::Then,
1582 Token::Word("printf".to_string()),
1583 Token::Word("yes".to_string()),
1584 Token::Semicolon,
1585 Token::Fi,
1586 ]
1587 );
1588 }
1589
1590 #[test]
1591 fn test_command_substitution_dollar_paren() {
1592 let shell_state = ShellState::new();
1593 let result = lex("echo $(pwd)", &shell_state).unwrap();
1594 assert_eq!(result.len(), 2);
1596 assert_eq!(result[0], Token::Word("echo".to_string()));
1597 assert!(matches!(result[1], Token::Word(_)));
1598 }
1599
1600 #[test]
1601 fn test_command_substitution_backticks() {
1602 let shell_state = ShellState::new();
1603 let result = lex("echo `pwd`", &shell_state).unwrap();
1604 assert_eq!(result.len(), 2);
1606 assert_eq!(result[0], Token::Word("echo".to_string()));
1607 assert!(matches!(result[1], Token::Word(_)));
1608 }
1609
1610 #[test]
1611 fn test_command_substitution_with_arguments() {
1612 let mut shell_state = ShellState::new();
1613 let tokens = lex("echo $(echo hello world)", &shell_state).unwrap();
1614 let result = expand_tokens(tokens, &mut shell_state);
1615 assert_eq!(
1616 result,
1617 vec![
1618 Token::Word("echo".to_string()),
1619 Token::Word("hello world".to_string())
1620 ]
1621 );
1622 }
1623
1624 #[test]
1625 fn test_command_substitution_backticks_with_arguments() {
1626 let mut shell_state = ShellState::new();
1627 let tokens = lex("echo `echo hello world`", &shell_state).unwrap();
1628 let result = expand_tokens(tokens, &mut shell_state);
1629 assert_eq!(
1630 result,
1631 vec![
1632 Token::Word("echo".to_string()),
1633 Token::Word("hello world".to_string())
1634 ]
1635 );
1636 }
1637
1638 #[test]
1639 fn test_command_substitution_failure_fallback() {
1640 let shell_state = ShellState::new();
1641 let result = lex("echo $(nonexistent_command)", &shell_state).unwrap();
1642 assert_eq!(
1643 result,
1644 vec![
1645 Token::Word("echo".to_string()),
1646 Token::Word("$(nonexistent_command)".to_string())
1647 ]
1648 );
1649 }
1650
1651 #[test]
1652 fn test_command_substitution_backticks_failure_fallback() {
1653 let shell_state = ShellState::new();
1654 let result = lex("echo `nonexistent_command`", &shell_state).unwrap();
1655 assert_eq!(
1656 result,
1657 vec![
1658 Token::Word("echo".to_string()),
1659 Token::Word("`nonexistent_command`".to_string())
1660 ]
1661 );
1662 }
1663
1664 #[test]
1665 fn test_command_substitution_with_variables() {
1666 let mut shell_state = ShellState::new();
1667 shell_state.set_var("TEST_VAR", "test_value".to_string());
1668 let tokens = lex("echo $(echo $TEST_VAR)", &shell_state).unwrap();
1669 let result = expand_tokens(tokens, &mut shell_state);
1670 assert_eq!(
1671 result,
1672 vec![
1673 Token::Word("echo".to_string()),
1674 Token::Word("test_value".to_string())
1675 ]
1676 );
1677 }
1678
1679 #[test]
1680 fn test_command_substitution_in_assignment() {
1681 let mut shell_state = ShellState::new();
1682 let tokens = lex("MY_VAR=$(echo hello)", &shell_state).unwrap();
1683 let result = expand_tokens(tokens, &mut shell_state);
1684 assert_eq!(result, vec![Token::Word("MY_VAR=hello".to_string())]);
1686 }
1687
1688 #[test]
1689 fn test_command_substitution_backticks_in_assignment() {
1690 let mut shell_state = ShellState::new();
1691 let tokens = lex("MY_VAR=`echo hello`", &shell_state).unwrap();
1692 let result = expand_tokens(tokens, &mut shell_state);
1693 assert_eq!(
1695 result,
1696 vec![
1697 Token::Word("MY_VAR=".to_string()),
1698 Token::Word("hello".to_string())
1699 ]
1700 );
1701 }
1702
1703 #[test]
1704 fn test_command_substitution_with_quotes() {
1705 let mut shell_state = ShellState::new();
1706 let tokens = lex("echo \"$(echo hello world)\"", &shell_state).unwrap();
1707 let result = expand_tokens(tokens, &mut shell_state);
1708 assert_eq!(
1709 result,
1710 vec![
1711 Token::Word("echo".to_string()),
1712 Token::Word("hello world".to_string())
1713 ]
1714 );
1715 }
1716
1717 #[test]
1718 fn test_command_substitution_backticks_with_quotes() {
1719 let mut shell_state = ShellState::new();
1720 let tokens = lex("echo \"`echo hello world`\"", &shell_state).unwrap();
1721 let result = expand_tokens(tokens, &mut shell_state);
1722 assert_eq!(
1723 result,
1724 vec![
1725 Token::Word("echo".to_string()),
1726 Token::Word("hello world".to_string())
1727 ]
1728 );
1729 }
1730
1731 #[test]
1732 fn test_command_substitution_empty_output() {
1733 let mut shell_state = ShellState::new();
1734 let tokens = lex("echo $(true)", &shell_state).unwrap();
1735 let result = expand_tokens(tokens, &mut shell_state);
1736 assert_eq!(result, vec![Token::Word("echo".to_string())]);
1738 }
1739
1740 #[test]
1741 fn test_command_substitution_multiple_spaces() {
1742 let mut shell_state = ShellState::new();
1743 let tokens = lex("echo $(echo 'hello world')", &shell_state).unwrap();
1744 let result = expand_tokens(tokens, &mut shell_state);
1745 assert_eq!(
1746 result,
1747 vec![
1748 Token::Word("echo".to_string()),
1749 Token::Word("hello world".to_string())
1750 ]
1751 );
1752 }
1753
1754 #[test]
1755 fn test_command_substitution_with_newlines() {
1756 let mut shell_state = ShellState::new();
1757 let tokens = lex("echo $(printf 'hello\nworld')", &shell_state).unwrap();
1758 let result = expand_tokens(tokens, &mut shell_state);
1759 assert_eq!(
1760 result,
1761 vec![
1762 Token::Word("echo".to_string()),
1763 Token::Word("hello\nworld".to_string())
1764 ]
1765 );
1766 }
1767
1768 #[test]
1769 fn test_command_substitution_special_characters() {
1770 let shell_state = ShellState::new();
1771 let result = lex("echo $(echo '$#@^&*()')", &shell_state).unwrap();
1772 println!("Special chars test result: {:?}", result);
1773 assert_eq!(result.len(), 2);
1776 assert_eq!(result[0], Token::Word("echo".to_string()));
1777 assert!(matches!(result[1], Token::Word(_)));
1778 }
1779
1780 #[test]
1781 fn test_nested_command_substitution() {
1782 let shell_state = ShellState::new();
1785 let result = lex("echo $(echo $(pwd))", &shell_state).unwrap();
1786 assert_eq!(result.len(), 2);
1788 assert_eq!(result[0], Token::Word("echo".to_string()));
1789 assert!(matches!(result[1], Token::Word(_)));
1790 }
1791
1792 #[test]
1793 fn test_command_substitution_in_pipeline() {
1794 let shell_state = ShellState::new();
1795 let result = lex("$(echo hello) | cat", &shell_state).unwrap();
1796 println!("Pipeline test result: {:?}", result);
1797 assert_eq!(result.len(), 3);
1798 assert!(matches!(result[0], Token::Word(_)));
1799 assert_eq!(result[1], Token::Pipe);
1800 assert_eq!(result[2], Token::Word("cat".to_string()));
1801 }
1802
1803 #[test]
1804 fn test_command_substitution_with_redirection() {
1805 let shell_state = ShellState::new();
1806 let result = lex("$(echo hello) > output.txt", &shell_state).unwrap();
1807 assert_eq!(result.len(), 3);
1808 assert!(matches!(result[0], Token::Word(_)));
1809 assert_eq!(result[1], Token::RedirOut);
1810 assert_eq!(result[2], Token::Word("output.txt".to_string()));
1811 }
1812
1813 #[test]
1814 fn test_variable_in_quotes_with_pipe() {
1815 let mut shell_state = ShellState::new();
1816 shell_state.set_var("PATH", "/usr/bin:/bin".to_string());
1817 let tokens = lex("echo \"$PATH\" | tr ':' '\\n'", &shell_state).unwrap();
1818 let result = expand_tokens(tokens, &mut shell_state);
1819 assert_eq!(
1820 result,
1821 vec![
1822 Token::Word("echo".to_string()),
1823 Token::Word("/usr/bin:/bin".to_string()),
1824 Token::Pipe,
1825 Token::Word("tr".to_string()),
1826 Token::Word(":".to_string()),
1827 Token::Word("\\n".to_string())
1828 ]
1829 );
1830 }
1831
1832 #[test]
1833 fn test_expand_aliases_simple() {
1834 let mut shell_state = ShellState::new();
1835 shell_state.set_alias("ll", "ls -l".to_string());
1836 let tokens = vec![Token::Word("ll".to_string())];
1837 let result = expand_aliases(tokens, &shell_state, &mut HashSet::new()).unwrap();
1838 assert_eq!(
1839 result,
1840 vec![Token::Word("ls".to_string()), Token::Word("-l".to_string())]
1841 );
1842 }
1843
1844 #[test]
1845 fn test_expand_aliases_with_args() {
1846 let mut shell_state = ShellState::new();
1847 shell_state.set_alias("ll", "ls -l".to_string());
1848 let tokens = vec![
1849 Token::Word("ll".to_string()),
1850 Token::Word("/tmp".to_string()),
1851 ];
1852 let result = expand_aliases(tokens, &shell_state, &mut HashSet::new()).unwrap();
1853 assert_eq!(
1854 result,
1855 vec![
1856 Token::Word("ls".to_string()),
1857 Token::Word("-l".to_string()),
1858 Token::Word("/tmp".to_string())
1859 ]
1860 );
1861 }
1862
1863 #[test]
1864 fn test_expand_aliases_no_alias() {
1865 let shell_state = ShellState::new();
1866 let tokens = vec![Token::Word("ls".to_string())];
1867 let result = expand_aliases(tokens.clone(), &shell_state, &mut HashSet::new()).unwrap();
1868 assert_eq!(result, tokens);
1869 }
1870
1871 #[test]
1872 fn test_expand_aliases_chained() {
1873 let mut shell_state = ShellState::new();
1877 shell_state.set_alias("a", "b".to_string());
1878 shell_state.set_alias("b", "a".to_string());
1879 let tokens = vec![Token::Word("a".to_string())];
1880 let result = expand_aliases(tokens, &shell_state, &mut HashSet::new());
1881 assert!(result.is_ok());
1883 assert_eq!(result.unwrap(), vec![Token::Word("a".to_string())]);
1884 }
1885
1886 #[test]
1887 fn test_arithmetic_expansion_simple() {
1888 let mut shell_state = ShellState::new();
1889 let tokens = lex("echo $((2 + 3))", &shell_state).unwrap();
1890 let result = expand_tokens(tokens, &mut shell_state);
1891 assert_eq!(
1892 result,
1893 vec![
1894 Token::Word("echo".to_string()),
1895 Token::Word("5".to_string())
1896 ]
1897 );
1898 }
1899
1900 #[test]
1901 fn test_arithmetic_expansion_with_variables() {
1902 let mut shell_state = ShellState::new();
1903 shell_state.set_var("x", "10".to_string());
1904 shell_state.set_var("y", "20".to_string());
1905 let tokens = lex("echo $((x + y * 2))", &shell_state).unwrap();
1906 let result = expand_tokens(tokens, &mut shell_state);
1907 assert_eq!(
1908 result,
1909 vec![
1910 Token::Word("echo".to_string()),
1911 Token::Word("50".to_string()) ]
1913 );
1914 }
1915
1916 #[test]
1917 fn test_arithmetic_expansion_comparison() {
1918 let mut shell_state = ShellState::new();
1919 let tokens = lex("echo $((5 > 3))", &shell_state).unwrap();
1920 let result = expand_tokens(tokens, &mut shell_state);
1921 assert_eq!(
1922 result,
1923 vec![
1924 Token::Word("echo".to_string()),
1925 Token::Word("1".to_string()) ]
1927 );
1928 }
1929
1930 #[test]
1931 fn test_arithmetic_expansion_complex() {
1932 let mut shell_state = ShellState::new();
1933 shell_state.set_var("a", "3".to_string());
1934 let tokens = lex("echo $((a * 2 + 5))", &shell_state).unwrap();
1935 let result = expand_tokens(tokens, &mut shell_state);
1936 assert_eq!(
1937 result,
1938 vec![
1939 Token::Word("echo".to_string()),
1940 Token::Word("11".to_string()) ]
1942 );
1943 }
1944
1945 #[test]
1946 fn test_arithmetic_expansion_unmatched_parentheses() {
1947 let mut shell_state = ShellState::new();
1948 let tokens = lex("echo $((2 + 3", &shell_state).unwrap();
1949 let result = expand_tokens(tokens, &mut shell_state);
1950 assert_eq!(result.len(), 2);
1952 assert_eq!(result[0], Token::Word("echo".to_string()));
1953 let second_token = &result[1];
1955 if let Token::Word(s) = second_token {
1956 assert!(
1957 s.starts_with("$((") && s.contains("2") && s.contains("3"),
1958 "Expected unmatched arithmetic to be kept as literal, got: {}",
1959 s
1960 );
1961 } else {
1962 panic!("Expected Word token");
1963 }
1964 }
1965
1966 #[test]
1967 fn test_arithmetic_expansion_division_by_zero() {
1968 let mut shell_state = ShellState::new();
1969 let tokens = lex("echo $((5 / 0))", &shell_state).unwrap();
1970 let result = expand_tokens(tokens, &mut shell_state);
1971 assert_eq!(result.len(), 2);
1973 assert_eq!(result[0], Token::Word("echo".to_string()));
1974 if let Token::Word(s) = &result[1] {
1976 assert!(
1977 s.contains("Division by zero"),
1978 "Expected division by zero error, got: {}",
1979 s
1980 );
1981 } else {
1982 panic!("Expected Word token");
1983 }
1984 }
1985
1986 #[test]
1987 fn test_parameter_expansion_simple() {
1988 let mut shell_state = ShellState::new();
1989 shell_state.set_var("TEST_VAR", "hello world".to_string());
1990 let result = lex("echo ${TEST_VAR}", &shell_state).unwrap();
1991 assert_eq!(
1992 result,
1993 vec![
1994 Token::Word("echo".to_string()),
1995 Token::Word("hello world".to_string())
1996 ]
1997 );
1998 }
1999
2000 #[test]
2001 fn test_parameter_expansion_unset_variable() {
2002 let shell_state = ShellState::new();
2003 let result = lex("echo ${UNSET_VAR}", &shell_state).unwrap();
2004 assert_eq!(
2005 result,
2006 vec![Token::Word("echo".to_string()), Token::Word("".to_string())]
2007 );
2008 }
2009
2010 #[test]
2011 fn test_parameter_expansion_default() {
2012 let shell_state = ShellState::new();
2013 let result = lex("echo ${UNSET_VAR:-default}", &shell_state).unwrap();
2014 assert_eq!(
2015 result,
2016 vec![
2017 Token::Word("echo".to_string()),
2018 Token::Word("default".to_string())
2019 ]
2020 );
2021 }
2022
2023 #[test]
2024 fn test_parameter_expansion_default_set_variable() {
2025 let mut shell_state = ShellState::new();
2026 shell_state.set_var("TEST_VAR", "value".to_string());
2027 let result = lex("echo ${TEST_VAR:-default}", &shell_state).unwrap();
2028 assert_eq!(
2029 result,
2030 vec![
2031 Token::Word("echo".to_string()),
2032 Token::Word("value".to_string())
2033 ]
2034 );
2035 }
2036
2037 #[test]
2038 fn test_parameter_expansion_assign_default() {
2039 let shell_state = ShellState::new();
2040 let result = lex("echo ${UNSET_VAR:=default}", &shell_state).unwrap();
2041 assert_eq!(
2042 result,
2043 vec![
2044 Token::Word("echo".to_string()),
2045 Token::Word("default".to_string())
2046 ]
2047 );
2048 }
2049
2050 #[test]
2051 fn test_parameter_expansion_alternative() {
2052 let mut shell_state = ShellState::new();
2053 shell_state.set_var("TEST_VAR", "value".to_string());
2054 let result = lex("echo ${TEST_VAR:+replacement}", &shell_state).unwrap();
2055 assert_eq!(
2056 result,
2057 vec![
2058 Token::Word("echo".to_string()),
2059 Token::Word("replacement".to_string())
2060 ]
2061 );
2062 }
2063
2064 #[test]
2065 fn test_parameter_expansion_alternative_unset() {
2066 let shell_state = ShellState::new();
2067 let result = lex("echo ${UNSET_VAR:+replacement}", &shell_state).unwrap();
2068 assert_eq!(
2069 result,
2070 vec![Token::Word("echo".to_string()), Token::Word("".to_string())]
2071 );
2072 }
2073
2074 #[test]
2075 fn test_parameter_expansion_substring() {
2076 let mut shell_state = ShellState::new();
2077 shell_state.set_var("TEST_VAR", "hello world".to_string());
2078 let result = lex("echo ${TEST_VAR:6}", &shell_state).unwrap();
2079 assert_eq!(
2080 result,
2081 vec![
2082 Token::Word("echo".to_string()),
2083 Token::Word("world".to_string())
2084 ]
2085 );
2086 }
2087
2088 #[test]
2089 fn test_parameter_expansion_substring_with_length() {
2090 let mut shell_state = ShellState::new();
2091 shell_state.set_var("TEST_VAR", "hello world".to_string());
2092 let result = lex("echo ${TEST_VAR:0:5}", &shell_state).unwrap();
2093 assert_eq!(
2094 result,
2095 vec![
2096 Token::Word("echo".to_string()),
2097 Token::Word("hello".to_string())
2098 ]
2099 );
2100 }
2101
2102 #[test]
2103 fn test_parameter_expansion_length() {
2104 let mut shell_state = ShellState::new();
2105 shell_state.set_var("TEST_VAR", "hello".to_string());
2106 let result = lex("echo ${#TEST_VAR}", &shell_state).unwrap();
2107 assert_eq!(
2108 result,
2109 vec![
2110 Token::Word("echo".to_string()),
2111 Token::Word("5".to_string())
2112 ]
2113 );
2114 }
2115
2116 #[test]
2117 fn test_parameter_expansion_remove_shortest_prefix() {
2118 let mut shell_state = ShellState::new();
2119 shell_state.set_var("TEST_VAR", "prefix_hello".to_string());
2120 let result = lex("echo ${TEST_VAR#prefix_}", &shell_state).unwrap();
2121 assert_eq!(
2122 result,
2123 vec![
2124 Token::Word("echo".to_string()),
2125 Token::Word("hello".to_string())
2126 ]
2127 );
2128 }
2129
2130 #[test]
2131 fn test_parameter_expansion_remove_longest_prefix() {
2132 let mut shell_state = ShellState::new();
2133 shell_state.set_var("TEST_VAR", "prefix_prefix_hello".to_string());
2134 let result = lex("echo ${TEST_VAR##prefix_}", &shell_state).unwrap();
2135 assert_eq!(
2136 result,
2137 vec![
2138 Token::Word("echo".to_string()),
2139 Token::Word("prefix_hello".to_string())
2140 ]
2141 );
2142 }
2143
2144 #[test]
2145 fn test_parameter_expansion_remove_shortest_suffix() {
2146 let mut shell_state = ShellState::new();
2147 shell_state.set_var("TEST_VAR", "hello_suffix".to_string());
2148 let result = lex("echo ${TEST_VAR%suffix}", &shell_state).unwrap();
2149 assert_eq!(
2150 result,
2151 vec![
2152 Token::Word("echo".to_string()),
2153 Token::Word("hello_".to_string()) ]
2155 );
2156 }
2157
2158 #[test]
2159 fn test_parameter_expansion_remove_longest_suffix() {
2160 let mut shell_state = ShellState::new();
2161 shell_state.set_var("TEST_VAR", "hello_suffix_suffix".to_string());
2162 let result = lex("echo ${TEST_VAR%%suffix}", &shell_state).unwrap();
2163 assert_eq!(
2164 result,
2165 vec![
2166 Token::Word("echo".to_string()),
2167 Token::Word("hello_suffix_".to_string()) ]
2169 );
2170 }
2171
2172 #[test]
2173 fn test_parameter_expansion_substitute() {
2174 let mut shell_state = ShellState::new();
2175 shell_state.set_var("TEST_VAR", "hello world".to_string());
2176 let result = lex("echo ${TEST_VAR/world/universe}", &shell_state).unwrap();
2177 assert_eq!(
2178 result,
2179 vec![
2180 Token::Word("echo".to_string()),
2181 Token::Word("hello universe".to_string())
2182 ]
2183 );
2184 }
2185
2186 #[test]
2187 fn test_parameter_expansion_substitute_all() {
2188 let mut shell_state = ShellState::new();
2189 shell_state.set_var("TEST_VAR", "hello world world".to_string());
2190 let result = lex("echo ${TEST_VAR//world/universe}", &shell_state).unwrap();
2191 assert_eq!(
2192 result,
2193 vec![
2194 Token::Word("echo".to_string()),
2195 Token::Word("hello universe universe".to_string())
2196 ]
2197 );
2198 }
2199
2200 #[test]
2201 fn test_parameter_expansion_mixed_with_regular_variables() {
2202 let mut shell_state = ShellState::new();
2203 shell_state.set_var("VAR1", "value1".to_string());
2204 shell_state.set_var("VAR2", "value2".to_string());
2205 let tokens = lex("echo $VAR1 and ${VAR2}", &shell_state).unwrap();
2206 let result = expand_tokens(tokens, &mut shell_state);
2207 assert_eq!(
2208 result,
2209 vec![
2210 Token::Word("echo".to_string()),
2211 Token::Word("value1".to_string()),
2212 Token::Word("and".to_string()),
2213 Token::Word("value2".to_string())
2214 ]
2215 );
2216 }
2217
2218 #[test]
2219 fn test_parameter_expansion_in_double_quotes() {
2220 let mut shell_state = ShellState::new();
2221 shell_state.set_var("TEST_VAR", "hello".to_string());
2222 let result = lex("echo \"Value: ${TEST_VAR}\"", &shell_state).unwrap();
2223 assert_eq!(
2224 result,
2225 vec![
2226 Token::Word("echo".to_string()),
2227 Token::Word("Value: hello".to_string())
2228 ]
2229 );
2230 }
2231
2232 #[test]
2233 fn test_parameter_expansion_error_unset() {
2234 let shell_state = ShellState::new();
2235 let result = lex("echo ${UNSET_VAR:?error message}", &shell_state);
2236 assert!(result.is_ok());
2238 let tokens = result.unwrap();
2239 assert_eq!(tokens.len(), 3);
2240 assert_eq!(tokens[0], Token::Word("echo".to_string()));
2241 assert_eq!(tokens[1], Token::Word("${UNSET_VAR:?error}".to_string()));
2242 assert_eq!(tokens[2], Token::Word("message}".to_string()));
2243 }
2244
2245 #[test]
2246 fn test_parameter_expansion_complex_expression() {
2247 let mut shell_state = ShellState::new();
2248 shell_state.set_var("PATH", "/usr/bin:/bin:/usr/local/bin".to_string());
2249 let result = lex("echo ${PATH#/usr/bin:}", &shell_state).unwrap();
2250 assert_eq!(
2251 result,
2252 vec![
2253 Token::Word("echo".to_string()),
2254 Token::Word("/bin:/usr/local/bin".to_string())
2255 ]
2256 );
2257 }
2258
2259 #[test]
2260 fn test_local_keyword() {
2261 let shell_state = ShellState::new();
2262 let result = lex("local myvar", &shell_state).unwrap();
2263 assert_eq!(result, vec![Token::Local, Token::Word("myvar".to_string())]);
2264 }
2265
2266 #[test]
2267 fn test_local_keyword_in_function() {
2268 let shell_state = ShellState::new();
2269 let result = lex("local var=value", &shell_state).unwrap();
2270 assert_eq!(
2271 result,
2272 vec![Token::Local, Token::Word("var=value".to_string())]
2273 );
2274 }
2275
2276 #[test]
2277 fn test_single_quotes_with_semicolons() {
2278 let shell_state = ShellState::new();
2280 let result = lex("trap 'echo \"A\"; echo \"B\"' EXIT", &shell_state).unwrap();
2281 assert_eq!(
2282 result,
2283 vec![
2284 Token::Word("trap".to_string()),
2285 Token::Word("echo \"A\"; echo \"B\"".to_string()),
2286 Token::Word("EXIT".to_string())
2287 ]
2288 );
2289 }
2290
2291 #[test]
2292 fn test_double_quotes_with_semicolons() {
2293 let shell_state = ShellState::new();
2295 let result = lex("echo \"command1; command2\"", &shell_state).unwrap();
2296 assert_eq!(
2297 result,
2298 vec![
2299 Token::Word("echo".to_string()),
2300 Token::Word("command1; command2".to_string())
2301 ]
2302 );
2303 }
2304
2305 #[test]
2306 fn test_semicolons_outside_quotes() {
2307 let shell_state = ShellState::new();
2309 let result = lex("echo hello; echo world", &shell_state).unwrap();
2310 assert_eq!(
2311 result,
2312 vec![
2313 Token::Word("echo".to_string()),
2314 Token::Word("hello".to_string()),
2315 Token::Semicolon,
2316 Token::Word("echo".to_string()),
2317 Token::Word("world".to_string())
2318 ]
2319 );
2320 }
2321
2322 #[test]
2323 fn test_here_document_redirection() {
2324 let shell_state = ShellState::new();
2325 let result = lex("cat << EOF", &shell_state).unwrap();
2326 assert_eq!(
2327 result,
2328 vec![
2329 Token::Word("cat".to_string()),
2330 Token::RedirHereDoc("EOF".to_string(), false)
2331 ]
2332 );
2333 }
2334
2335 #[test]
2336 fn test_here_string_redirection() {
2337 let shell_state = ShellState::new();
2338 let result = lex("cat <<< \"hello world\"", &shell_state).unwrap();
2339 assert_eq!(
2340 result,
2341 vec![
2342 Token::Word("cat".to_string()),
2343 Token::RedirHereString("hello world".to_string())
2344 ]
2345 );
2346 }
2347
2348 #[test]
2349 fn test_here_document_with_quoted_delimiter() {
2350 let shell_state = ShellState::new();
2351 let result = lex("command << 'EOF'", &shell_state).unwrap();
2352 assert_eq!(
2353 result,
2354 vec![
2355 Token::Word("command".to_string()),
2356 Token::RedirHereDoc("EOF".to_string(), true) ]
2358 );
2359 }
2360
2361 #[test]
2362 fn test_here_string_without_quotes() {
2363 let shell_state = ShellState::new();
2364 let result = lex("grep <<< pattern", &shell_state).unwrap();
2365 assert_eq!(
2366 result,
2367 vec![
2368 Token::Word("grep".to_string()),
2369 Token::RedirHereString("pattern".to_string())
2370 ]
2371 );
2372 }
2373
2374 #[test]
2375 fn test_redirections_mixed() {
2376 let shell_state = ShellState::new();
2377 let result = lex(
2378 "cat < input.txt <<< \"fallback\" > output.txt",
2379 &shell_state,
2380 )
2381 .unwrap();
2382 assert_eq!(
2383 result,
2384 vec![
2385 Token::Word("cat".to_string()),
2386 Token::RedirIn,
2387 Token::Word("input.txt".to_string()),
2388 Token::RedirHereString("fallback".to_string()),
2389 Token::RedirOut,
2390 Token::Word("output.txt".to_string())
2391 ]
2392 );
2393 }
2394
2395 #[test]
2396 fn test_tilde_expansion_unquoted() {
2397 let _lock = ENV_LOCK.lock().unwrap();
2398 let shell_state = ShellState::new();
2399 let home = env::var("HOME").unwrap_or_else(|_| "/home/user".to_string());
2400 let result = lex("echo ~", &shell_state).unwrap();
2401 assert_eq!(
2402 result,
2403 vec![Token::Word("echo".to_string()), Token::Word(home)]
2404 );
2405 }
2406
2407 #[test]
2408 fn test_tilde_expansion_single_quoted() {
2409 let shell_state = ShellState::new();
2410 let result = lex("echo '~'", &shell_state).unwrap();
2411 assert_eq!(
2412 result,
2413 vec![
2414 Token::Word("echo".to_string()),
2415 Token::Word("~".to_string())
2416 ]
2417 );
2418 }
2419
2420 #[test]
2421 fn test_tilde_expansion_double_quoted() {
2422 let shell_state = ShellState::new();
2423 let result = lex("echo \"~\"", &shell_state).unwrap();
2424 assert_eq!(
2425 result,
2426 vec![
2427 Token::Word("echo".to_string()),
2428 Token::Word("~".to_string())
2429 ]
2430 );
2431 }
2432
2433 #[test]
2434 fn test_tilde_expansion_mixed_quotes() {
2435 let _lock = ENV_LOCK.lock().unwrap();
2436 let shell_state = ShellState::new();
2437 let home = env::var("HOME").unwrap_or_else(|_| "/home/user".to_string());
2438 let result = lex("echo ~ '~' \"~\"", &shell_state).unwrap();
2439 assert_eq!(
2440 result,
2441 vec![
2442 Token::Word("echo".to_string()),
2443 Token::Word(home),
2444 Token::Word("~".to_string()),
2445 Token::Word("~".to_string())
2446 ]
2447 );
2448 }
2449
2450 #[test]
2451 fn test_tilde_expansion_pwd() {
2452 let mut shell_state = ShellState::new();
2453
2454 let test_pwd = "/test/current/dir";
2456 shell_state.set_var("PWD", test_pwd.to_string());
2457
2458 let result = lex("echo ~+", &shell_state).unwrap();
2459 assert_eq!(
2460 result,
2461 vec![
2462 Token::Word("echo".to_string()),
2463 Token::Word(test_pwd.to_string())
2464 ]
2465 );
2466 }
2467
2468 #[test]
2469 fn test_tilde_expansion_oldpwd() {
2470 let mut shell_state = ShellState::new();
2471
2472 let test_oldpwd = "/test/old/dir";
2474 shell_state.set_var("OLDPWD", test_oldpwd.to_string());
2475
2476 let result = lex("echo ~-", &shell_state).unwrap();
2477 assert_eq!(
2478 result,
2479 vec![
2480 Token::Word("echo".to_string()),
2481 Token::Word(test_oldpwd.to_string())
2482 ]
2483 );
2484 }
2485
2486 #[test]
2487 fn test_tilde_expansion_pwd_unset() {
2488 let _lock = ENV_LOCK.lock().unwrap();
2489 let shell_state = ShellState::new();
2490
2491 let result = lex("echo ~+", &shell_state).unwrap();
2493 assert_eq!(result.len(), 2);
2494 assert_eq!(result[0], Token::Word("echo".to_string()));
2495
2496 if let Token::Word(path) = &result[1] {
2498 assert!(path.starts_with('/') || path == "~+");
2500 } else {
2501 panic!("Expected Word token");
2502 }
2503 }
2504
2505 #[test]
2506 fn test_tilde_expansion_oldpwd_unset() {
2507 let _lock = ENV_LOCK.lock().unwrap();
2509
2510 let original_oldpwd = env::var("OLDPWD").ok();
2512 unsafe {
2513 env::remove_var("OLDPWD");
2514 }
2515
2516 let shell_state = ShellState::new();
2517
2518 let result = lex("echo ~-", &shell_state).unwrap();
2520 assert_eq!(
2521 result,
2522 vec![
2523 Token::Word("echo".to_string()),
2524 Token::Word("~-".to_string())
2525 ]
2526 );
2527
2528 unsafe {
2530 if let Some(oldpwd) = original_oldpwd {
2531 env::set_var("OLDPWD", oldpwd);
2532 }
2533 }
2534 }
2535
2536 #[test]
2537 fn test_tilde_expansion_pwd_in_quotes() {
2538 let mut shell_state = ShellState::new();
2539 shell_state.set_var("PWD", "/test/dir".to_string());
2540
2541 let result = lex("echo '~+'", &shell_state).unwrap();
2543 assert_eq!(
2544 result,
2545 vec![
2546 Token::Word("echo".to_string()),
2547 Token::Word("~+".to_string())
2548 ]
2549 );
2550
2551 let result = lex("echo \"~+\"", &shell_state).unwrap();
2553 assert_eq!(
2554 result,
2555 vec![
2556 Token::Word("echo".to_string()),
2557 Token::Word("~+".to_string())
2558 ]
2559 );
2560 }
2561
2562 #[test]
2563 fn test_tilde_expansion_oldpwd_in_quotes() {
2564 let mut shell_state = ShellState::new();
2565 shell_state.set_var("OLDPWD", "/test/old".to_string());
2566
2567 let result = lex("echo '~-'", &shell_state).unwrap();
2569 assert_eq!(
2570 result,
2571 vec![
2572 Token::Word("echo".to_string()),
2573 Token::Word("~-".to_string())
2574 ]
2575 );
2576
2577 let result = lex("echo \"~-\"", &shell_state).unwrap();
2579 assert_eq!(
2580 result,
2581 vec![
2582 Token::Word("echo".to_string()),
2583 Token::Word("~-".to_string())
2584 ]
2585 );
2586 }
2587
2588 #[test]
2589 fn test_tilde_expansion_mixed() {
2590 let _lock = ENV_LOCK.lock().unwrap();
2591 let mut shell_state = ShellState::new();
2592 let home = env::var("HOME").unwrap_or_else(|_| "/home/user".to_string());
2593 shell_state.set_var("PWD", "/current".to_string());
2594 shell_state.set_var("OLDPWD", "/previous".to_string());
2595
2596 let result = lex("echo ~ ~+ ~-", &shell_state).unwrap();
2597 assert_eq!(
2598 result,
2599 vec![
2600 Token::Word("echo".to_string()),
2601 Token::Word(home),
2602 Token::Word("/current".to_string()),
2603 Token::Word("/previous".to_string())
2604 ]
2605 );
2606 }
2607
2608 #[test]
2609 fn test_tilde_expansion_not_at_start() {
2610 let mut shell_state = ShellState::new();
2611 shell_state.set_var("PWD", "/test".to_string());
2612
2613 let result = lex("echo prefix~+", &shell_state).unwrap();
2615 assert_eq!(
2616 result,
2617 vec![
2618 Token::Word("echo".to_string()),
2619 Token::Word("prefix~+".to_string())
2620 ]
2621 );
2622 }
2623
2624 #[test]
2625 fn test_tilde_expansion_username() {
2626 let shell_state = ShellState::new();
2627
2628 let result = lex("echo ~root", &shell_state).unwrap();
2630 assert_eq!(result.len(), 2);
2631 assert_eq!(result[0], Token::Word("echo".to_string()));
2632
2633 if let Token::Word(path) = &result[1] {
2635 assert!(path == "/root" || path == "~root");
2636 } else {
2637 panic!("Expected Word token");
2638 }
2639 }
2640
2641 #[test]
2642 fn test_tilde_expansion_username_with_path() {
2643 let shell_state = ShellState::new();
2644
2645 let result = lex("echo ~root/documents", &shell_state).unwrap();
2647 assert_eq!(result.len(), 2);
2648 assert_eq!(result[0], Token::Word("echo".to_string()));
2649
2650 if let Token::Word(path) = &result[1] {
2652 assert!(path == "/root/documents" || path == "~root/documents");
2653 } else {
2654 panic!("Expected Word token");
2655 }
2656 }
2657
2658 #[test]
2659 fn test_tilde_expansion_nonexistent_user() {
2660 let shell_state = ShellState::new();
2661
2662 let result = lex("echo ~nonexistentuser12345", &shell_state).unwrap();
2664 assert_eq!(
2665 result,
2666 vec![
2667 Token::Word("echo".to_string()),
2668 Token::Word("~nonexistentuser12345".to_string())
2669 ]
2670 );
2671 }
2672
2673 #[test]
2674 fn test_tilde_expansion_username_in_quotes() {
2675 let shell_state = ShellState::new();
2676
2677 let result = lex("echo '~root'", &shell_state).unwrap();
2679 assert_eq!(
2680 result,
2681 vec![
2682 Token::Word("echo".to_string()),
2683 Token::Word("~root".to_string())
2684 ]
2685 );
2686
2687 let result = lex("echo \"~root\"", &shell_state).unwrap();
2689 assert_eq!(
2690 result,
2691 vec![
2692 Token::Word("echo".to_string()),
2693 Token::Word("~root".to_string())
2694 ]
2695 );
2696 }
2697
2698 #[test]
2699 fn test_tilde_expansion_mixed_with_username() {
2700 let _lock = ENV_LOCK.lock().unwrap();
2701 let mut shell_state = ShellState::new();
2702 let home = env::var("HOME").unwrap_or_else(|_| "/home/user".to_string());
2703 shell_state.set_var("PWD", "/current".to_string());
2704
2705 let result = lex("echo ~ ~+ ~root", &shell_state).unwrap();
2707 assert_eq!(result.len(), 4);
2708 assert_eq!(result[0], Token::Word("echo".to_string()));
2709 assert_eq!(result[1], Token::Word(home));
2710 assert_eq!(result[2], Token::Word("/current".to_string()));
2711
2712 if let Token::Word(path) = &result[3] {
2714 assert!(path == "/root" || path == "~root");
2715 } else {
2716 panic!("Expected Word token");
2717 }
2718 }
2719
2720 #[test]
2721 fn test_tilde_expansion_username_with_special_chars() {
2722 let shell_state = ShellState::new();
2723
2724 let result = lex("echo ~user@host", &shell_state).unwrap();
2726 assert_eq!(result.len(), 2);
2727 assert_eq!(result[0], Token::Word("echo".to_string()));
2728
2729 if let Token::Word(path) = &result[1] {
2731 assert!(path.contains("@host") || path == "~user@host");
2733 } else {
2734 panic!("Expected Word token");
2735 }
2736 }
2737
2738 #[test]
2741 fn test_fd_output_redirection() {
2742 let shell_state = ShellState::new();
2743 let result = lex("command 2>errors.log", &shell_state).unwrap();
2744 assert_eq!(
2745 result,
2746 vec![
2747 Token::Word("command".to_string()),
2748 Token::RedirectFdOut(2, "errors.log".to_string())
2749 ]
2750 );
2751 }
2752
2753 #[test]
2754 fn test_fd_input_redirection() {
2755 let shell_state = ShellState::new();
2756 let result = lex("command 3<input.txt", &shell_state).unwrap();
2757 assert_eq!(
2758 result,
2759 vec![
2760 Token::Word("command".to_string()),
2761 Token::RedirectFdIn(3, "input.txt".to_string())
2762 ]
2763 );
2764 }
2765
2766 #[test]
2767 fn test_fd_append_redirection() {
2768 let shell_state = ShellState::new();
2769 let result = lex("command 2>>errors.log", &shell_state).unwrap();
2770 assert_eq!(
2771 result,
2772 vec![
2773 Token::Word("command".to_string()),
2774 Token::RedirectFdAppend(2, "errors.log".to_string())
2775 ]
2776 );
2777 }
2778
2779 #[test]
2780 fn test_fd_duplication_output() {
2781 let shell_state = ShellState::new();
2782 let result = lex("command 2>&1", &shell_state).unwrap();
2783 assert_eq!(
2784 result,
2785 vec![
2786 Token::Word("command".to_string()),
2787 Token::RedirectFdDup(2, 1)
2788 ]
2789 );
2790 }
2791
2792 #[test]
2793 fn test_fd_duplication_input() {
2794 let shell_state = ShellState::new();
2795 let result = lex("command 0<&3", &shell_state).unwrap();
2796 assert_eq!(
2797 result,
2798 vec![
2799 Token::Word("command".to_string()),
2800 Token::RedirectFdDup(0, 3)
2801 ]
2802 );
2803 }
2804
2805 #[test]
2806 fn test_fd_close_output() {
2807 let shell_state = ShellState::new();
2808 let result = lex("command 2>&-", &shell_state).unwrap();
2809 assert_eq!(
2810 result,
2811 vec![
2812 Token::Word("command".to_string()),
2813 Token::RedirectFdClose(2)
2814 ]
2815 );
2816 }
2817
2818 #[test]
2819 fn test_fd_close_input() {
2820 let shell_state = ShellState::new();
2821 let result = lex("command 3<&-", &shell_state).unwrap();
2822 assert_eq!(
2823 result,
2824 vec![
2825 Token::Word("command".to_string()),
2826 Token::RedirectFdClose(3)
2827 ]
2828 );
2829 }
2830
2831 #[test]
2832 fn test_fd_read_write() {
2833 let shell_state = ShellState::new();
2834 let result = lex("command 3<>file.txt", &shell_state).unwrap();
2835 assert_eq!(
2836 result,
2837 vec![
2838 Token::Word("command".to_string()),
2839 Token::RedirectFdInOut(3, "file.txt".to_string())
2840 ]
2841 );
2842 }
2843
2844 #[test]
2845 fn test_fd_read_write_default() {
2846 let shell_state = ShellState::new();
2847 let result = lex("command <>file.txt", &shell_state).unwrap();
2848 assert_eq!(
2849 result,
2850 vec![
2851 Token::Word("command".to_string()),
2852 Token::RedirectFdInOut(0, "file.txt".to_string())
2853 ]
2854 );
2855 }
2856
2857 #[test]
2858 fn test_multiple_fd_redirections() {
2859 let shell_state = ShellState::new();
2860 let result = lex("command 2>err.log 3<input.txt 4>>append.log", &shell_state).unwrap();
2861 assert_eq!(
2862 result,
2863 vec![
2864 Token::Word("command".to_string()),
2865 Token::RedirectFdOut(2, "err.log".to_string()),
2866 Token::RedirectFdIn(3, "input.txt".to_string()),
2867 Token::RedirectFdAppend(4, "append.log".to_string())
2868 ]
2869 );
2870 }
2871
2872 #[test]
2873 fn test_fd_redirection_with_pipe() {
2874 let shell_state = ShellState::new();
2875 let result = lex("command 2>&1 | grep error", &shell_state).unwrap();
2876 assert_eq!(
2877 result,
2878 vec![
2879 Token::Word("command".to_string()),
2880 Token::RedirectFdDup(2, 1),
2881 Token::Pipe,
2882 Token::Word("grep".to_string()),
2883 Token::Word("error".to_string())
2884 ]
2885 );
2886 }
2887
2888 #[test]
2889 fn test_fd_numbers_0_through_9() {
2890 let shell_state = ShellState::new();
2891
2892 let result = lex("cmd 0<file", &shell_state).unwrap();
2894 assert_eq!(result[1], Token::RedirectFdIn(0, "file".to_string()));
2895
2896 let result = lex("cmd 9>file", &shell_state).unwrap();
2898 assert_eq!(result[1], Token::RedirectFdOut(9, "file".to_string()));
2899 }
2900
2901 #[test]
2902 fn test_fd_swap_pattern() {
2903 let shell_state = ShellState::new();
2904 let result = lex("command 3>&1 1>&2 2>&3 3>&-", &shell_state).unwrap();
2905 assert_eq!(
2906 result,
2907 vec![
2908 Token::Word("command".to_string()),
2909 Token::RedirectFdDup(3, 1),
2910 Token::RedirectFdDup(1, 2),
2911 Token::RedirectFdDup(2, 3),
2912 Token::RedirectFdClose(3)
2913 ]
2914 );
2915 }
2916
2917 #[test]
2918 fn test_backward_compat_simple_output() {
2919 let shell_state = ShellState::new();
2920 let result = lex("echo hello > output.txt", &shell_state).unwrap();
2921 assert_eq!(
2922 result,
2923 vec![
2924 Token::Word("echo".to_string()),
2925 Token::Word("hello".to_string()),
2926 Token::RedirOut,
2927 Token::Word("output.txt".to_string())
2928 ]
2929 );
2930 }
2931
2932 #[test]
2933 fn test_backward_compat_simple_input() {
2934 let shell_state = ShellState::new();
2935 let result = lex("cat < input.txt", &shell_state).unwrap();
2936 assert_eq!(
2937 result,
2938 vec![
2939 Token::Word("cat".to_string()),
2940 Token::RedirIn,
2941 Token::Word("input.txt".to_string())
2942 ]
2943 );
2944 }
2945
2946 #[test]
2947 fn test_backward_compat_append() {
2948 let shell_state = ShellState::new();
2949 let result = lex("echo hello >> output.txt", &shell_state).unwrap();
2950 assert_eq!(
2951 result,
2952 vec![
2953 Token::Word("echo".to_string()),
2954 Token::Word("hello".to_string()),
2955 Token::RedirAppend,
2956 Token::Word("output.txt".to_string())
2957 ]
2958 );
2959 }
2960
2961 #[test]
2962 fn test_fd_with_spaces() {
2963 let shell_state = ShellState::new();
2964 let result = lex("command 2> errors.log", &shell_state).unwrap();
2965 assert_eq!(
2966 result,
2967 vec![
2968 Token::Word("command".to_string()),
2969 Token::RedirectFdOut(2, "errors.log".to_string())
2970 ]
2971 );
2972 }
2973
2974 #[test]
2975 fn test_fd_no_space() {
2976 let shell_state = ShellState::new();
2977 let result = lex("command 2>errors.log", &shell_state).unwrap();
2978 assert_eq!(
2979 result,
2980 vec![
2981 Token::Word("command".to_string()),
2982 Token::RedirectFdOut(2, "errors.log".to_string())
2983 ]
2984 );
2985 }
2986
2987 #[test]
2988 fn test_fd_dup_to_self() {
2989 let shell_state = ShellState::new();
2990 let result = lex("command 1>&1", &shell_state).unwrap();
2991 assert_eq!(
2992 result,
2993 vec![
2994 Token::Word("command".to_string()),
2995 Token::RedirectFdDup(1, 1)
2996 ]
2997 );
2998 }
2999
3000 #[test]
3001 fn test_stderr_to_stdout() {
3002 let shell_state = ShellState::new();
3003 let result = lex("ls /nonexistent 2>&1", &shell_state).unwrap();
3004 assert_eq!(
3005 result,
3006 vec![
3007 Token::Word("ls".to_string()),
3008 Token::Word("/nonexistent".to_string()),
3009 Token::RedirectFdDup(2, 1)
3010 ]
3011 );
3012 }
3013
3014 #[test]
3015 fn test_stdout_to_stderr() {
3016 let shell_state = ShellState::new();
3017 let result = lex("echo error 1>&2", &shell_state).unwrap();
3018 assert_eq!(
3019 result,
3020 vec![
3021 Token::Word("echo".to_string()),
3022 Token::Word("error".to_string()),
3023 Token::RedirectFdDup(1, 2)
3024 ]
3025 );
3026 }
3027
3028 #[test]
3029 fn test_combined_redirections() {
3030 let shell_state = ShellState::new();
3031 let result = lex("command >output.txt 2>&1", &shell_state).unwrap();
3032 assert_eq!(
3033 result,
3034 vec![
3035 Token::Word("command".to_string()),
3036 Token::RedirOut,
3037 Token::Word("output.txt".to_string()),
3038 Token::RedirectFdDup(2, 1)
3039 ]
3040 );
3041 }
3042
3043 #[test]
3044 fn test_fd_with_variable_filename() {
3045 let shell_state = ShellState::new();
3046 let result = lex("command 2>$LOGFILE", &shell_state).unwrap();
3047 assert_eq!(
3048 result,
3049 vec![
3050 Token::Word("command".to_string()),
3051 Token::RedirectFdOut(2, "$LOGFILE".to_string())
3052 ]
3053 );
3054 }
3055
3056 #[test]
3057 fn test_invalid_fd_dup_no_target() {
3058 let shell_state = ShellState::new();
3059 let result = lex("command 2>&", &shell_state);
3060 assert!(result.is_err());
3061 assert!(
3062 result
3063 .unwrap_err()
3064 .contains("expected fd number or '-' after >&")
3065 );
3066 }
3067
3068 #[test]
3069 fn test_invalid_fd_close_input_no_dash() {
3070 let shell_state = ShellState::new();
3071 let result = lex("command 3<&", &shell_state);
3072 assert!(result.is_err());
3073 assert!(
3074 result
3075 .unwrap_err()
3076 .contains("expected fd number or '-' after <&")
3077 );
3078 }
3079
3080 #[test]
3081 fn test_fd_inout_no_filename() {
3082 let shell_state = ShellState::new();
3083 let result = lex("command 3<>", &shell_state);
3084 assert!(result.is_err());
3085 assert!(result.unwrap_err().contains("expected filename after <>"));
3086 }
3087
3088 #[test]
3089 fn test_fd_output_no_filename() {
3090 let shell_state = ShellState::new();
3091 let result = lex("command 2>", &shell_state);
3092 assert!(result.is_err());
3093 assert!(result.unwrap_err().contains("expected filename after >"));
3094 }
3095
3096 #[test]
3097 fn test_fd_input_no_filename() {
3098 let shell_state = ShellState::new();
3099 let result = lex("command 3<", &shell_state);
3100 assert!(result.is_err());
3101 assert!(result.unwrap_err().contains("expected filename after <"));
3102 }
3103
3104 #[test]
3105 fn test_fd_append_no_filename() {
3106 let shell_state = ShellState::new();
3107 let result = lex("command 2>>", &shell_state);
3108 assert!(result.is_err());
3109 assert!(result.unwrap_err().contains("expected filename after >>"));
3110 }
3111}