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