1use std::cell::RefCell;
2use std::fs::File;
3use std::io::{BufRead, BufReader, pipe};
4use std::process::{Command, Stdio};
5use std::rc::Rc;
6
7use super::parser::{Ast, ShellCommand};
8use super::state::ShellState;
9
10fn execute_and_capture_output(ast: Ast, shell_state: &mut ShellState) -> Result<String, String> {
13 let (reader, writer) = pipe().map_err(|e| format!("Failed to create pipe: {}", e))?;
15
16 match &ast {
21 Ast::Pipeline(commands) if commands.len() == 1 => {
22 let cmd = &commands[0];
23 if cmd.args.is_empty() {
24 return Ok(String::new());
25 }
26
27 let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
29 let expanded_args = expand_wildcards(&var_expanded_args)
30 .map_err(|e| format!("Wildcard expansion failed: {}", e))?;
31
32 if expanded_args.is_empty() {
33 return Ok(String::new());
34 }
35
36 if shell_state.get_function(&expanded_args[0]).is_some() {
38 let previous_capture = shell_state.capture_output.clone();
40
41 let capture_buffer = Rc::new(RefCell::new(Vec::new()));
43 shell_state.capture_output = Some(capture_buffer.clone());
44
45 let function_call_ast = Ast::FunctionCall {
47 name: expanded_args[0].clone(),
48 args: expanded_args[1..].to_vec(),
49 };
50
51 let exit_code = execute(function_call_ast, shell_state);
52
53 let captured = capture_buffer.borrow().clone();
55 let output = String::from_utf8_lossy(&captured).trim_end().to_string();
56
57 shell_state.capture_output = previous_capture;
59
60 if exit_code == 0 {
61 Ok(output)
62 } else {
63 Err(format!("Function failed with exit code {}", exit_code))
64 }
65 } else if crate::builtins::is_builtin(&expanded_args[0]) {
66 let temp_cmd = ShellCommand {
67 args: expanded_args,
68 input: cmd.input.clone(),
69 output: None, append: None,
71 here_doc_delimiter: None,
72 here_doc_quoted: false,
73 here_string_content: None,
74 };
75
76 let exit_code = crate::builtins::execute_builtin(
78 &temp_cmd,
79 shell_state,
80 Some(Box::new(writer)),
81 );
82
83 drop(temp_cmd); let mut output = String::new();
86 use std::io::Read;
87 let mut reader = reader;
88 reader
89 .read_to_string(&mut output)
90 .map_err(|e| format!("Failed to read output: {}", e))?;
91
92 if exit_code == 0 {
93 Ok(output.trim_end().to_string())
94 } else {
95 Err(format!("Command failed with exit code {}", exit_code))
96 }
97 } else {
98 drop(writer); let mut command = Command::new(&expanded_args[0]);
102 command.args(&expanded_args[1..]);
103 command.stdout(Stdio::piped());
104 command.stderr(Stdio::null()); let child_env = shell_state.get_env_for_child();
108 command.env_clear();
109 for (key, value) in child_env {
110 command.env(key, value);
111 }
112
113 let output = command
114 .output()
115 .map_err(|e| format!("Failed to execute command: {}", e))?;
116
117 if output.status.success() {
118 Ok(String::from_utf8_lossy(&output.stdout)
119 .trim_end()
120 .to_string())
121 } else {
122 Err(format!(
123 "Command failed with exit code {}",
124 output.status.code().unwrap_or(1)
125 ))
126 }
127 }
128 }
129 _ => {
130 drop(writer);
134
135 Err("Complex command substitutions not yet fully supported".to_string())
138 }
139 }
140}
141
142fn expand_variables_in_args(args: &[String], shell_state: &mut ShellState) -> Vec<String> {
143 let mut expanded_args = Vec::new();
144
145 for arg in args {
146 let expanded_arg = expand_variables_in_string(arg, shell_state);
148 expanded_args.push(expanded_arg);
149 }
150
151 expanded_args
152}
153
154pub fn expand_variables_in_string(input: &str, shell_state: &mut ShellState) -> String {
155 let mut result = String::new();
156 let mut chars = input.chars().peekable();
157
158 while let Some(ch) = chars.next() {
159 if ch == '$' {
160 if let Some(&'(') = chars.peek() {
162 chars.next(); if let Some(&'(') = chars.peek() {
166 chars.next(); let mut arithmetic_expr = String::new();
169 let mut paren_depth = 1;
170 let mut found_closing = false;
171
172 while let Some(c) = chars.next() {
173 if c == '(' {
174 paren_depth += 1;
175 arithmetic_expr.push(c);
176 } else if c == ')' {
177 paren_depth -= 1;
178 if paren_depth == 0 {
179 if let Some(&')') = chars.peek() {
181 chars.next(); found_closing = true;
183 break;
184 } else {
185 result.push_str("$((");
187 result.push_str(&arithmetic_expr);
188 result.push(')');
189 break;
190 }
191 }
192 arithmetic_expr.push(c);
193 } else {
194 arithmetic_expr.push(c);
195 }
196 }
197
198 if found_closing {
199 let mut expanded_expr = String::new();
203 let mut expr_chars = arithmetic_expr.chars().peekable();
204
205 while let Some(ch) = expr_chars.next() {
206 if ch == '$' {
207 let mut var_name = String::new();
209 if let Some(&c) = expr_chars.peek() {
210 if c == '?'
211 || c == '$'
212 || c == '0'
213 || c == '#'
214 || c == '*'
215 || c == '@'
216 || c.is_ascii_digit()
217 {
218 var_name.push(c);
219 expr_chars.next();
220 } else {
221 while let Some(&c) = expr_chars.peek() {
222 if c.is_alphanumeric() || c == '_' {
223 var_name.push(c);
224 expr_chars.next();
225 } else {
226 break;
227 }
228 }
229 }
230 }
231
232 if !var_name.is_empty() {
233 if let Some(value) = shell_state.get_var(&var_name) {
234 expanded_expr.push_str(&value);
235 } else {
236 expanded_expr.push('0');
238 }
239 } else {
240 expanded_expr.push('$');
241 }
242 } else {
243 expanded_expr.push(ch);
244 }
245 }
246
247 match crate::arithmetic::evaluate_arithmetic_expression(
248 &expanded_expr,
249 shell_state,
250 ) {
251 Ok(value) => {
252 result.push_str(&value.to_string());
253 }
254 Err(e) => {
255 if shell_state.colors_enabled {
257 result.push_str(&format!(
258 "{}arithmetic error: {}{}",
259 shell_state.color_scheme.error, e, "\x1b[0m"
260 ));
261 } else {
262 result.push_str(&format!("arithmetic error: {}", e));
263 }
264 }
265 }
266 } else {
267 result.push_str("$((");
269 result.push_str(&arithmetic_expr);
270 }
272 continue;
273 }
274
275 let mut sub_command = String::new();
277 let mut paren_depth = 1;
278
279 for c in chars.by_ref() {
280 if c == '(' {
281 paren_depth += 1;
282 sub_command.push(c);
283 } else if c == ')' {
284 paren_depth -= 1;
285 if paren_depth == 0 {
286 break;
287 }
288 sub_command.push(c);
289 } else {
290 sub_command.push(c);
291 }
292 }
293
294 if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
297 let expanded_tokens = match crate::lexer::expand_aliases(
299 tokens,
300 shell_state,
301 &mut std::collections::HashSet::new(),
302 ) {
303 Ok(t) => t,
304 Err(_) => {
305 result.push_str("$(");
307 result.push_str(&sub_command);
308 result.push(')');
309 continue;
310 }
311 };
312
313 if let Ok(ast) = crate::parser::parse(expanded_tokens) {
314 match execute_and_capture_output(ast, shell_state) {
316 Ok(output) => {
317 result.push_str(&output);
318 }
319 Err(_) => {
320 result.push_str("$(");
322 result.push_str(&sub_command);
323 result.push(')');
324 }
325 }
326 } else {
327 let tokens_str = sub_command.trim();
329 if tokens_str.contains(' ') {
330 let parts: Vec<&str> = tokens_str.split_whitespace().collect();
332 if let Some(first_token) = parts.first()
333 && shell_state.get_function(first_token).is_some()
334 {
335 let function_call = Ast::FunctionCall {
337 name: first_token.to_string(),
338 args: parts[1..].iter().map(|s| s.to_string()).collect(),
339 };
340 match execute_and_capture_output(function_call, shell_state) {
341 Ok(output) => {
342 result.push_str(&output);
343 continue;
344 }
345 Err(_) => {
346 }
348 }
349 }
350 }
351 result.push_str("$(");
353 result.push_str(&sub_command);
354 result.push(')');
355 }
356 } else {
357 result.push_str("$(");
359 result.push_str(&sub_command);
360 result.push(')');
361 }
362 } else {
363 let mut var_name = String::new();
365 let mut next_ch = chars.peek();
366
367 if let Some(&c) = next_ch {
369 if c == '?' || c == '$' || c == '0' || c == '#' || c == '*' || c == '@' {
370 var_name.push(c);
371 chars.next(); } else if c.is_ascii_digit() {
373 var_name.push(c);
375 chars.next();
376 } else {
377 while let Some(&c) = next_ch {
379 if c.is_alphanumeric() || c == '_' {
380 var_name.push(c);
381 chars.next(); next_ch = chars.peek();
383 } else {
384 break;
385 }
386 }
387 }
388 }
389
390 if !var_name.is_empty() {
391 if let Some(value) = shell_state.get_var(&var_name) {
392 result.push_str(&value);
393 } else {
394 if var_name.chars().next().unwrap().is_ascii_digit()
397 || var_name == "?"
398 || var_name == "$"
399 || var_name == "0"
400 || var_name == "#"
401 || var_name == "*"
402 || var_name == "@"
403 {
404 } else {
406 result.push('$');
408 result.push_str(&var_name);
409 }
410 }
411 } else {
412 result.push('$');
413 }
414 }
415 } else if ch == '`' {
416 let mut sub_command = String::new();
418
419 for c in chars.by_ref() {
420 if c == '`' {
421 break;
422 }
423 sub_command.push(c);
424 }
425
426 if let Ok(tokens) = crate::lexer::lex(&sub_command, shell_state) {
428 let expanded_tokens = match crate::lexer::expand_aliases(
430 tokens,
431 shell_state,
432 &mut std::collections::HashSet::new(),
433 ) {
434 Ok(t) => t,
435 Err(_) => {
436 result.push('`');
438 result.push_str(&sub_command);
439 result.push('`');
440 continue;
441 }
442 };
443
444 if let Ok(ast) = crate::parser::parse(expanded_tokens) {
445 match execute_and_capture_output(ast, shell_state) {
447 Ok(output) => {
448 result.push_str(&output);
449 }
450 Err(_) => {
451 result.push('`');
453 result.push_str(&sub_command);
454 result.push('`');
455 }
456 }
457 } else {
458 result.push('`');
460 result.push_str(&sub_command);
461 result.push('`');
462 }
463 } else {
464 result.push('`');
466 result.push_str(&sub_command);
467 result.push('`');
468 }
469 } else {
470 result.push(ch);
471 }
472 }
473
474 result
475}
476
477fn expand_wildcards(args: &[String]) -> Result<Vec<String>, String> {
478 let mut expanded_args = Vec::new();
479
480 for arg in args {
481 if arg.contains('*') || arg.contains('?') || arg.contains('[') {
482 match glob::glob(arg) {
484 Ok(paths) => {
485 let mut matches: Vec<String> = paths
486 .filter_map(|p| p.ok())
487 .map(|p| p.to_string_lossy().to_string())
488 .collect();
489 if matches.is_empty() {
490 expanded_args.push(arg.clone());
492 } else {
493 matches.sort();
495 expanded_args.extend(matches);
496 }
497 }
498 Err(_e) => {
499 expanded_args.push(arg.clone());
501 }
502 }
503 } else {
504 expanded_args.push(arg.clone());
505 }
506 }
507 Ok(expanded_args)
508}
509
510fn collect_here_document_content(delimiter: &str, shell_state: &mut ShellState) -> String {
514 if let Some(content) = shell_state.pending_heredoc_content.take() {
516 return content;
517 }
518
519 let stdin = std::io::stdin();
521 let mut reader = BufReader::new(stdin.lock());
522 let mut content = String::new();
523 let mut line = String::new();
524
525 loop {
526 line.clear();
527 match reader.read_line(&mut line) {
528 Ok(0) => {
529 break;
531 }
532 Ok(_) => {
533 let line_content = line.trim_end();
535 if line_content == delimiter {
536 break;
538 } else {
539 content.push_str(&line);
541 }
542 }
543 Err(e) => {
544 if shell_state.colors_enabled {
545 eprintln!(
546 "{}Error reading here-document content: {}\x1b[0m",
547 shell_state.color_scheme.error, e
548 );
549 } else {
550 eprintln!("Error reading here-document content: {}", e);
551 }
552 break;
553 }
554 }
555 }
556
557 content
558}
559
560pub fn execute_trap_handler(trap_cmd: &str, shell_state: &mut ShellState) -> i32 {
563 let saved_exit_code = shell_state.last_exit_code;
565
566 let result = match crate::lexer::lex(trap_cmd, shell_state) {
572 Ok(tokens) => {
573 match crate::lexer::expand_aliases(
574 tokens,
575 shell_state,
576 &mut std::collections::HashSet::new(),
577 ) {
578 Ok(expanded_tokens) => {
579 match crate::parser::parse(expanded_tokens) {
580 Ok(ast) => execute(ast, shell_state),
581 Err(_) => {
582 saved_exit_code
584 }
585 }
586 }
587 Err(_) => {
588 saved_exit_code
590 }
591 }
592 }
593 Err(_) => {
594 saved_exit_code
596 }
597 };
598
599 shell_state.last_exit_code = saved_exit_code;
601
602 result
603}
604
605pub fn execute(ast: Ast, shell_state: &mut ShellState) -> i32 {
606 match ast {
607 Ast::Assignment { var, value } => {
608 let expanded_value = expand_variables_in_string(&value, shell_state);
610 shell_state.set_var(&var, expanded_value);
611 0
612 }
613 Ast::LocalAssignment { var, value } => {
614 let expanded_value = expand_variables_in_string(&value, shell_state);
616 shell_state.set_local_var(&var, expanded_value);
617 0
618 }
619 Ast::Pipeline(commands) => {
620 if commands.is_empty() {
621 return 0;
622 }
623
624 if commands.len() == 1 {
625 execute_single_command(&commands[0], shell_state)
627 } else {
628 execute_pipeline(&commands, shell_state)
630 }
631 }
632 Ast::Sequence(asts) => {
633 let mut exit_code = 0;
634 for ast in asts {
635 exit_code = execute(ast, shell_state);
636
637 if shell_state.is_returning() {
639 return exit_code;
640 }
641
642 if shell_state.exit_requested {
644 return shell_state.exit_code;
645 }
646 }
647 exit_code
648 }
649 Ast::If {
650 branches,
651 else_branch,
652 } => {
653 for (condition, then_branch) in branches {
654 let cond_exit = execute(*condition, shell_state);
655 if cond_exit == 0 {
656 let exit_code = execute(*then_branch, shell_state);
657
658 if shell_state.is_returning() {
660 return exit_code;
661 }
662
663 return exit_code;
664 }
665 }
666 if let Some(else_b) = else_branch {
667 let exit_code = execute(*else_b, shell_state);
668
669 if shell_state.is_returning() {
671 return exit_code;
672 }
673
674 exit_code
675 } else {
676 0
677 }
678 }
679 Ast::Case {
680 word,
681 cases,
682 default,
683 } => {
684 for (patterns, branch) in cases {
685 for pattern in &patterns {
686 if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
687 if glob_pattern.matches(&word) {
688 let exit_code = execute(branch, shell_state);
689
690 if shell_state.is_returning() {
692 return exit_code;
693 }
694
695 return exit_code;
696 }
697 } else {
698 if &word == pattern {
700 let exit_code = execute(branch, shell_state);
701
702 if shell_state.is_returning() {
704 return exit_code;
705 }
706
707 return exit_code;
708 }
709 }
710 }
711 }
712 if let Some(def) = default {
713 let exit_code = execute(*def, shell_state);
714
715 if shell_state.is_returning() {
717 return exit_code;
718 }
719
720 exit_code
721 } else {
722 0
723 }
724 }
725 Ast::For {
726 variable,
727 items,
728 body,
729 } => {
730 let mut exit_code = 0;
731
732 for item in items {
734 crate::state::process_pending_signals(shell_state);
736
737 if shell_state.exit_requested {
739 return shell_state.exit_code;
740 }
741
742 shell_state.set_var(&variable, item.clone());
744
745 exit_code = execute(*body.clone(), shell_state);
747
748 if shell_state.is_returning() {
750 return exit_code;
751 }
752
753 if shell_state.exit_requested {
755 return shell_state.exit_code;
756 }
757 }
758
759 exit_code
760 }
761 Ast::While { condition, body } => {
762 let mut exit_code = 0;
763
764 loop {
766 let cond_exit = execute(*condition.clone(), shell_state);
768
769 if shell_state.is_returning() {
771 return cond_exit;
772 }
773
774 if shell_state.exit_requested {
776 return shell_state.exit_code;
777 }
778
779 if cond_exit != 0 {
781 break;
782 }
783
784 exit_code = execute(*body.clone(), shell_state);
786
787 if shell_state.is_returning() {
789 return exit_code;
790 }
791
792 if shell_state.exit_requested {
794 return shell_state.exit_code;
795 }
796 }
797
798 exit_code
799 }
800 Ast::FunctionDefinition { name, body } => {
801 shell_state.define_function(name.clone(), *body);
803 0
804 }
805 Ast::FunctionCall { name, args } => {
806 if let Some(function_body) = shell_state.get_function(&name).cloned() {
807 if shell_state.function_depth >= shell_state.max_recursion_depth {
809 eprintln!(
810 "Function recursion limit ({}) exceeded",
811 shell_state.max_recursion_depth
812 );
813 return 1;
814 }
815
816 shell_state.enter_function();
818
819 let old_positional = shell_state.positional_params.clone();
821
822 shell_state.set_positional_params(args.clone());
824
825 let exit_code = execute(function_body, shell_state);
827
828 if shell_state.is_returning() {
830 let return_value = shell_state.get_return_value().unwrap_or(0);
831
832 shell_state.set_positional_params(old_positional);
834
835 shell_state.exit_function();
837
838 shell_state.clear_return();
840
841 return return_value;
843 }
844
845 shell_state.set_positional_params(old_positional);
847
848 shell_state.exit_function();
850
851 exit_code
852 } else {
853 eprintln!("Function '{}' not found", name);
854 1
855 }
856 }
857 Ast::Return { value } => {
858 if shell_state.function_depth == 0 {
860 eprintln!("Return statement outside of function");
861 return 1;
862 }
863
864 let exit_code = if let Some(ref val) = value {
866 val.parse::<i32>().unwrap_or(0)
867 } else {
868 0
869 };
870
871 shell_state.set_return(exit_code);
873
874 exit_code
876 }
877 Ast::And { left, right } => {
878 let left_exit = execute(*left, shell_state);
880
881 if shell_state.is_returning() {
883 return left_exit;
884 }
885
886 if left_exit == 0 {
888 execute(*right, shell_state)
889 } else {
890 left_exit
891 }
892 }
893 Ast::Or { left, right } => {
894 let left_exit = execute(*left, shell_state);
896
897 if shell_state.is_returning() {
899 return left_exit;
900 }
901
902 if left_exit != 0 {
904 execute(*right, shell_state)
905 } else {
906 left_exit
907 }
908 }
909 }
910}
911
912fn execute_single_command(cmd: &ShellCommand, shell_state: &mut ShellState) -> i32 {
913 if cmd.args.is_empty() {
914 return 0;
915 }
916
917 let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
919 let expanded_args = match expand_wildcards(&var_expanded_args) {
920 Ok(args) => args,
921 Err(_) => return 1,
922 };
923
924 if expanded_args.is_empty() {
925 return 0;
926 }
927
928 if shell_state.get_function(&expanded_args[0]).is_some() {
930 let function_call = Ast::FunctionCall {
932 name: expanded_args[0].clone(),
933 args: expanded_args[1..].to_vec(),
934 };
935 return execute(function_call, shell_state);
936 }
937
938 if crate::builtins::is_builtin(&expanded_args[0]) {
939 let temp_cmd = ShellCommand {
941 args: expanded_args,
942 input: cmd.input.clone(),
943 output: cmd.output.clone(),
944 append: cmd.append.clone(),
945 here_doc_delimiter: None,
946 here_doc_quoted: false,
947 here_string_content: None,
948 };
949
950 if let Some(ref capture_buffer) = shell_state.capture_output.clone() {
952 struct CaptureWriter {
954 buffer: Rc<RefCell<Vec<u8>>>,
955 }
956 impl std::io::Write for CaptureWriter {
957 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
958 self.buffer.borrow_mut().extend_from_slice(buf);
959 Ok(buf.len())
960 }
961 fn flush(&mut self) -> std::io::Result<()> {
962 Ok(())
963 }
964 }
965 let writer = CaptureWriter {
966 buffer: capture_buffer.clone(),
967 };
968 crate::builtins::execute_builtin(&temp_cmd, shell_state, Some(Box::new(writer)))
969 } else {
970 crate::builtins::execute_builtin(&temp_cmd, shell_state, None)
971 }
972 } else {
973 let mut env_assignments = Vec::new();
976 let mut command_start_idx = 0;
977
978 for (idx, arg) in expanded_args.iter().enumerate() {
979 if let Some(eq_pos) = arg.find('=')
981 && eq_pos > 0
982 {
983 let var_part = &arg[..eq_pos];
984 if var_part
986 .chars()
987 .next()
988 .map(|c| c.is_alphabetic() || c == '_')
989 .unwrap_or(false)
990 && var_part.chars().all(|c| c.is_alphanumeric() || c == '_')
991 {
992 env_assignments.push(arg.clone());
993 command_start_idx = idx + 1;
994 continue;
995 }
996 }
997 break;
999 }
1000
1001 let has_command = command_start_idx < expanded_args.len();
1003
1004 if !has_command {
1007 for assignment in &env_assignments {
1008 if let Some(eq_pos) = assignment.find('=') {
1009 let var_name = &assignment[..eq_pos];
1010 let var_value = &assignment[eq_pos + 1..];
1011 shell_state.set_var(var_name, var_value.to_string());
1012 }
1013 }
1014 }
1015
1016 let mut command = if has_command {
1018 let mut cmd = Command::new(&expanded_args[command_start_idx]);
1019 cmd.args(&expanded_args[command_start_idx + 1..]);
1020
1021 let mut child_env = shell_state.get_env_for_child();
1023
1024 for assignment in env_assignments {
1026 if let Some(eq_pos) = assignment.find('=') {
1027 let var_name = assignment[..eq_pos].to_string();
1028 let var_value = assignment[eq_pos + 1..].to_string();
1029 child_env.insert(var_name, var_value);
1030 }
1031 }
1032
1033 cmd.env_clear();
1034 for (key, value) in child_env {
1035 cmd.env(key, value);
1036 }
1037
1038 let capturing = shell_state.capture_output.is_some();
1040 if capturing {
1041 cmd.stdout(Stdio::piped());
1042 }
1043
1044 Some(cmd)
1045 } else {
1046 None
1047 };
1048
1049 if let Some(ref input_file) = cmd.input {
1051 let expanded_input = expand_variables_in_string(input_file, shell_state);
1052 if let Some(ref mut command) = command {
1053 match File::open(&expanded_input) {
1054 Ok(file) => {
1055 command.stdin(Stdio::from(file));
1056 }
1057 Err(e) => {
1058 if shell_state.colors_enabled {
1059 eprintln!(
1060 "{}Error opening input file '{}{}",
1061 shell_state.color_scheme.error,
1062 input_file,
1063 &format!("': {}\x1b[0m", e)
1064 );
1065 } else {
1066 eprintln!("Error opening input file '{}': {}", input_file, e);
1067 }
1068 return 1;
1069 }
1070 }
1071 } else {
1072 match File::open(&expanded_input) {
1074 Ok(_) => {
1075 }
1077 Err(e) => {
1078 if shell_state.colors_enabled {
1079 eprintln!(
1080 "{}Error opening input file '{}{}",
1081 shell_state.color_scheme.error,
1082 input_file,
1083 &format!("': {}\x1b[0m", e)
1084 );
1085 } else {
1086 eprintln!("Error opening input file '{}': {}", input_file, e);
1087 }
1088 return 1;
1089 }
1090 }
1091 }
1092 } else if let Some(ref delimiter) = cmd.here_doc_delimiter {
1093 let here_doc_content = collect_here_document_content(delimiter, shell_state);
1095 let expanded_content = if cmd.here_doc_quoted {
1098 here_doc_content.clone() } else {
1100 expand_variables_in_string(&here_doc_content, shell_state)
1101 };
1102
1103 if let Some(ref mut command) = command {
1104 let pipe_result = pipe();
1105 match pipe_result {
1106 Ok((reader, mut writer)) => {
1107 use std::io::Write;
1108 if let Err(e) = writeln!(writer, "{}", expanded_content) {
1109 if shell_state.colors_enabled {
1110 eprintln!(
1111 "{}Error writing here-document content: {}\x1b[0m",
1112 shell_state.color_scheme.error, e
1113 );
1114 } else {
1115 eprintln!("Error writing here-document content: {}", e);
1116 }
1117 return 1;
1118 }
1119 command.stdin(Stdio::from(reader));
1121 }
1122 Err(e) => {
1123 if shell_state.colors_enabled {
1124 eprintln!(
1125 "{}Error creating pipe for here-document: {}\x1b[0m",
1126 shell_state.color_scheme.error, e
1127 );
1128 } else {
1129 eprintln!("Error creating pipe for here-document: {}", e);
1130 }
1131 return 1;
1132 }
1133 }
1134 }
1135 } else if let Some(ref content) = cmd.here_string_content {
1137 let expanded_content = expand_variables_in_string(content, shell_state);
1139
1140 if let Some(ref mut command) = command {
1141 let pipe_result = pipe();
1142 match pipe_result {
1143 Ok((reader, mut writer)) => {
1144 use std::io::Write;
1145 if let Err(e) = write!(writer, "{}", expanded_content) {
1146 if shell_state.colors_enabled {
1147 eprintln!(
1148 "{}Error writing here-string content: {}\x1b[0m",
1149 shell_state.color_scheme.error, e
1150 );
1151 } else {
1152 eprintln!("Error writing here-string content: {}", e);
1153 }
1154 return 1;
1155 }
1156 command.stdin(Stdio::from(reader));
1158 }
1159 Err(e) => {
1160 if shell_state.colors_enabled {
1161 eprintln!(
1162 "{}Error creating pipe for here-string: {}\x1b[0m",
1163 shell_state.color_scheme.error, e
1164 );
1165 } else {
1166 eprintln!("Error creating pipe for here-string: {}", e);
1167 }
1168 return 1;
1169 }
1170 }
1171 }
1172 }
1174
1175 if let Some(ref output_file) = cmd.output {
1177 let expanded_output = expand_variables_in_string(output_file, shell_state);
1178 match File::create(&expanded_output) {
1179 Ok(file) => {
1180 if let Some(ref mut command) = command {
1181 command.stdout(Stdio::from(file));
1182 }
1183 }
1185 Err(e) => {
1186 if shell_state.colors_enabled {
1187 eprintln!(
1188 "{}Error creating output file '{}{}",
1189 shell_state.color_scheme.error,
1190 output_file,
1191 &format!("': {}\x1b[0m", e)
1192 );
1193 } else {
1194 eprintln!("Error creating output file '{}': {}", output_file, e);
1195 }
1196 return 1;
1197 }
1198 }
1199 } else if let Some(ref append_file) = cmd.append {
1200 let expanded_append = expand_variables_in_string(append_file, shell_state);
1201 match File::options()
1202 .append(true)
1203 .create(true)
1204 .open(&expanded_append)
1205 {
1206 Ok(file) => {
1207 if let Some(ref mut command) = command {
1208 command.stdout(Stdio::from(file));
1209 }
1210 }
1212 Err(e) => {
1213 if shell_state.colors_enabled {
1214 eprintln!(
1215 "{}Error opening append file '{}{}",
1216 shell_state.color_scheme.error,
1217 append_file,
1218 &format!("': {}\x1b[0m", e)
1219 );
1220 } else {
1221 eprintln!("Error opening append file '{}': {}", append_file, e);
1222 }
1223 return 1;
1224 }
1225 }
1226 }
1227
1228 let Some(mut command) = command else {
1230 return 0;
1231 };
1232
1233 let capturing = shell_state.capture_output.is_some();
1235
1236 match command.spawn() {
1237 Ok(mut child) => {
1238 if capturing && let Some(mut stdout) = child.stdout.take() {
1240 use std::io::Read;
1241 let mut output = Vec::new();
1242 if stdout.read_to_end(&mut output).is_ok()
1243 && let Some(ref capture_buffer) = shell_state.capture_output
1244 {
1245 capture_buffer.borrow_mut().extend_from_slice(&output);
1246 }
1247 }
1248
1249 match child.wait() {
1250 Ok(status) => status.code().unwrap_or(0),
1251 Err(e) => {
1252 if shell_state.colors_enabled {
1253 eprintln!(
1254 "{}Error waiting for command: {}\x1b[0m",
1255 shell_state.color_scheme.error, e
1256 );
1257 } else {
1258 eprintln!("Error waiting for command: {}", e);
1259 }
1260 1
1261 }
1262 }
1263 }
1264 Err(e) => {
1265 if shell_state.colors_enabled {
1266 eprintln!(
1267 "{}Command spawn error: {}\x1b[0m",
1268 shell_state.color_scheme.error, e
1269 );
1270 } else {
1271 eprintln!("Command spawn error: {}", e);
1272 }
1273 1
1274 }
1275 }
1276 }
1277}
1278
1279fn execute_pipeline(commands: &[ShellCommand], shell_state: &mut ShellState) -> i32 {
1280 let mut exit_code = 0;
1281 let mut previous_stdout = None;
1282
1283 for (i, cmd) in commands.iter().enumerate() {
1284 if cmd.args.is_empty() {
1285 continue;
1286 }
1287
1288 let is_last = i == commands.len() - 1;
1289
1290 let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
1292 let expanded_args = match expand_wildcards(&var_expanded_args) {
1293 Ok(args) => args,
1294 Err(_) => return 1,
1295 };
1296
1297 if expanded_args.is_empty() {
1298 continue;
1299 }
1300
1301 if crate::builtins::is_builtin(&expanded_args[0]) {
1302 let temp_cmd = ShellCommand {
1305 args: expanded_args,
1306 input: cmd.input.clone(),
1307 output: cmd.output.clone(),
1308 append: cmd.append.clone(),
1309 here_doc_delimiter: None,
1310 here_doc_quoted: false,
1311 here_string_content: None,
1312 };
1313 if !is_last {
1314 let (reader, writer) = match pipe() {
1316 Ok(p) => p,
1317 Err(e) => {
1318 if shell_state.colors_enabled {
1319 eprintln!(
1320 "{}Error creating pipe for builtin: {}\x1b[0m",
1321 shell_state.color_scheme.error, e
1322 );
1323 } else {
1324 eprintln!("Error creating pipe for builtin: {}", e);
1325 }
1326 return 1;
1327 }
1328 };
1329 exit_code = crate::builtins::execute_builtin(
1331 &temp_cmd,
1332 shell_state,
1333 Some(Box::new(writer)),
1334 );
1335 previous_stdout = Some(Stdio::from(reader));
1337 } else {
1338 exit_code = crate::builtins::execute_builtin(&temp_cmd, shell_state, None);
1340 previous_stdout = None;
1341 }
1342 } else {
1343 let mut command = Command::new(&expanded_args[0]);
1344 command.args(&expanded_args[1..]);
1345
1346 let child_env = shell_state.get_env_for_child();
1348 command.env_clear();
1349 for (key, value) in child_env {
1350 command.env(key, value);
1351 }
1352
1353 if let Some(prev) = previous_stdout.take() {
1355 command.stdin(prev);
1356 }
1357
1358 if !is_last {
1360 command.stdout(Stdio::piped());
1361 }
1362
1363 if i == 0 {
1365 if let Some(ref input_file) = cmd.input {
1366 let expanded_input = expand_variables_in_string(input_file, shell_state);
1367 match File::open(&expanded_input) {
1368 Ok(file) => {
1369 command.stdin(Stdio::from(file));
1370 }
1371 Err(e) => {
1372 if shell_state.colors_enabled {
1373 eprintln!(
1374 "{}Error opening input file '{}{}",
1375 shell_state.color_scheme.error,
1376 input_file,
1377 &format!("': {}\x1b[0m", e)
1378 );
1379 } else {
1380 eprintln!("Error opening input file '{}': {}", input_file, e);
1381 }
1382 return 1;
1383 }
1384 }
1385 } else if let Some(ref delimiter) = cmd.here_doc_delimiter {
1386 let here_doc_content = collect_here_document_content(delimiter, shell_state);
1388 let expanded_content = if cmd.here_doc_quoted {
1391 here_doc_content } else {
1393 expand_variables_in_string(&here_doc_content, shell_state)
1394 };
1395 let pipe_result = pipe();
1396 match pipe_result {
1397 Ok((reader, mut writer)) => {
1398 use std::io::Write;
1399 if let Err(e) = writeln!(writer, "{}", expanded_content) {
1400 if shell_state.colors_enabled {
1401 eprintln!(
1402 "{}Error writing here-document content: {}\x1b[0m",
1403 shell_state.color_scheme.error, e
1404 );
1405 } else {
1406 eprintln!("Error writing here-document content: {}", e);
1407 }
1408 return 1;
1409 }
1410 command.stdin(Stdio::from(reader));
1411 }
1412 Err(e) => {
1413 if shell_state.colors_enabled {
1414 eprintln!(
1415 "{}Error creating pipe for here-document: {}\x1b[0m",
1416 shell_state.color_scheme.error, e
1417 );
1418 } else {
1419 eprintln!("Error creating pipe for here-document: {}", e);
1420 }
1421 return 1;
1422 }
1423 }
1424 } else if let Some(ref content) = cmd.here_string_content {
1425 let expanded_content = expand_variables_in_string(content, shell_state);
1427 let pipe_result = pipe();
1428 match pipe_result {
1429 Ok((reader, mut writer)) => {
1430 use std::io::Write;
1431 if let Err(e) = write!(writer, "{}", expanded_content) {
1432 if shell_state.colors_enabled {
1433 eprintln!(
1434 "{}Error writing here-string content: {}\x1b[0m",
1435 shell_state.color_scheme.error, e
1436 );
1437 } else {
1438 eprintln!("Error writing here-string content: {}", e);
1439 }
1440 return 1;
1441 }
1442 command.stdin(Stdio::from(reader));
1443 }
1444 Err(e) => {
1445 if shell_state.colors_enabled {
1446 eprintln!(
1447 "{}Error creating pipe for here-string: {}\x1b[0m",
1448 shell_state.color_scheme.error, e
1449 );
1450 } else {
1451 eprintln!("Error creating pipe for here-string: {}", e);
1452 }
1453 return 1;
1454 }
1455 }
1456 }
1457 }
1458
1459 if is_last {
1461 if let Some(ref output_file) = cmd.output {
1462 let expanded_output = expand_variables_in_string(output_file, shell_state);
1463 match File::create(&expanded_output) {
1464 Ok(file) => {
1465 command.stdout(Stdio::from(file));
1466 }
1467 Err(e) => {
1468 if shell_state.colors_enabled {
1469 eprintln!(
1470 "{}Error creating output file '{}{}",
1471 shell_state.color_scheme.error,
1472 output_file,
1473 &format!("': {}\x1b[0m", e)
1474 );
1475 } else {
1476 eprintln!("Error creating output file '{}': {}", output_file, e);
1477 }
1478 return 1;
1479 }
1480 }
1481 } else if let Some(ref append_file) = cmd.append {
1482 let expanded_append = expand_variables_in_string(append_file, shell_state);
1483 match File::options()
1484 .append(true)
1485 .create(true)
1486 .open(&expanded_append)
1487 {
1488 Ok(file) => {
1489 command.stdout(Stdio::from(file));
1490 }
1491 Err(e) => {
1492 if shell_state.colors_enabled {
1493 eprintln!(
1494 "{}Error opening append file '{}{}",
1495 shell_state.color_scheme.error,
1496 append_file,
1497 &format!("': {}\x1b[0m", e)
1498 );
1499 } else {
1500 eprintln!("Error opening append file '{}': {}", append_file, e);
1501 }
1502 return 1;
1503 }
1504 }
1505 }
1506 }
1507
1508 match command.spawn() {
1509 Ok(mut child) => {
1510 if !is_last {
1511 previous_stdout = child.stdout.take().map(Stdio::from);
1512 }
1513 match child.wait() {
1514 Ok(status) => {
1515 exit_code = status.code().unwrap_or(0);
1516 }
1517 Err(e) => {
1518 if shell_state.colors_enabled {
1519 eprintln!(
1520 "{}Error waiting for command: {}\x1b[0m",
1521 shell_state.color_scheme.error, e
1522 );
1523 } else {
1524 eprintln!("Error waiting for command: {}", e);
1525 }
1526 exit_code = 1;
1527 }
1528 }
1529 }
1530 Err(e) => {
1531 if shell_state.colors_enabled {
1532 eprintln!(
1533 "{}Error spawning command '{}{}",
1534 shell_state.color_scheme.error,
1535 expanded_args[0],
1536 &format!("': {}\x1b[0m", e)
1537 );
1538 } else {
1539 eprintln!("Error spawning command '{}': {}", expanded_args[0], e);
1540 }
1541 exit_code = 1;
1542 }
1543 }
1544 }
1545 }
1546
1547 exit_code
1548}
1549
1550#[cfg(test)]
1551mod tests {
1552 use super::*;
1553
1554 #[test]
1555 fn test_execute_single_command_builtin() {
1556 let cmd = ShellCommand {
1557 args: vec!["true".to_string()],
1558 input: None,
1559 output: None,
1560 append: None,
1561 here_doc_delimiter: None,
1562 here_doc_quoted: false,
1563 here_string_content: None,
1564 };
1565 let mut shell_state = ShellState::new();
1566 let exit_code = execute_single_command(&cmd, &mut shell_state);
1567 assert_eq!(exit_code, 0);
1568 }
1569
1570 #[test]
1572 fn test_execute_single_command_external() {
1573 let cmd = ShellCommand {
1574 args: vec!["true".to_string()], input: None,
1576 output: None,
1577 append: None,
1578 here_doc_delimiter: None,
1579 here_doc_quoted: false,
1580 here_string_content: None,
1581 };
1582 let mut shell_state = ShellState::new();
1583 let exit_code = execute_single_command(&cmd, &mut shell_state);
1584 assert_eq!(exit_code, 0);
1585 }
1586
1587 #[test]
1588 fn test_execute_single_command_external_nonexistent() {
1589 let cmd = ShellCommand {
1590 args: vec!["nonexistent_command".to_string()],
1591 input: None,
1592 output: None,
1593 append: None,
1594 here_doc_delimiter: None,
1595 here_doc_quoted: false,
1596 here_string_content: None,
1597 };
1598 let mut shell_state = ShellState::new();
1599 let exit_code = execute_single_command(&cmd, &mut shell_state);
1600 assert_eq!(exit_code, 1); }
1602
1603 #[test]
1604 fn test_execute_pipeline() {
1605 let commands = vec![
1606 ShellCommand {
1607 args: vec!["printf".to_string(), "hello".to_string()],
1608 input: None,
1609 output: None,
1610 append: None,
1611 here_doc_delimiter: None,
1612 here_doc_quoted: false,
1613 here_string_content: None,
1614 },
1615 ShellCommand {
1616 args: vec!["cat".to_string()], input: None,
1618 output: None,
1619 append: None,
1620 here_doc_delimiter: None,
1621 here_doc_quoted: false,
1622 here_string_content: None,
1623 },
1624 ];
1625 let mut shell_state = ShellState::new();
1626 let exit_code = execute_pipeline(&commands, &mut shell_state);
1627 assert_eq!(exit_code, 0);
1628 }
1629
1630 #[test]
1631 fn test_execute_empty_pipeline() {
1632 let commands = vec![];
1633 let mut shell_state = ShellState::new();
1634 let exit_code = execute(Ast::Pipeline(commands), &mut shell_state);
1635 assert_eq!(exit_code, 0);
1636 }
1637
1638 #[test]
1639 fn test_execute_single_command() {
1640 let ast = Ast::Pipeline(vec![ShellCommand {
1641 args: vec!["true".to_string()],
1642 input: None,
1643 output: None,
1644 append: None,
1645 here_doc_delimiter: None,
1646 here_doc_quoted: false,
1647 here_string_content: None,
1648 }]);
1649 let mut shell_state = ShellState::new();
1650 let exit_code = execute(ast, &mut shell_state);
1651 assert_eq!(exit_code, 0);
1652 }
1653
1654 #[test]
1655 fn test_execute_function_definition() {
1656 let ast = Ast::FunctionDefinition {
1657 name: "test_func".to_string(),
1658 body: Box::new(Ast::Pipeline(vec![ShellCommand {
1659 args: vec!["echo".to_string(), "hello".to_string()],
1660 input: None,
1661 output: None,
1662 append: None,
1663 here_doc_delimiter: None,
1664 here_doc_quoted: false,
1665 here_string_content: None,
1666 }])),
1667 };
1668 let mut shell_state = ShellState::new();
1669 let exit_code = execute(ast, &mut shell_state);
1670 assert_eq!(exit_code, 0);
1671
1672 assert!(shell_state.get_function("test_func").is_some());
1674 }
1675
1676 #[test]
1677 fn test_execute_function_call() {
1678 let mut shell_state = ShellState::new();
1680 shell_state.define_function(
1681 "test_func".to_string(),
1682 Ast::Pipeline(vec![ShellCommand {
1683 args: vec!["echo".to_string(), "hello".to_string()],
1684 input: None,
1685 output: None,
1686 append: None,
1687 here_doc_delimiter: None,
1688 here_doc_quoted: false,
1689 here_string_content: None,
1690 }]),
1691 );
1692
1693 let ast = Ast::FunctionCall {
1695 name: "test_func".to_string(),
1696 args: vec![],
1697 };
1698 let exit_code = execute(ast, &mut shell_state);
1699 assert_eq!(exit_code, 0);
1700 }
1701
1702 #[test]
1703 fn test_execute_function_call_with_args() {
1704 let mut shell_state = ShellState::new();
1706 shell_state.define_function(
1707 "test_func".to_string(),
1708 Ast::Pipeline(vec![ShellCommand {
1709 args: vec!["echo".to_string(), "arg1".to_string()],
1710 input: None,
1711 output: None,
1712 append: None,
1713 here_doc_delimiter: None,
1714 here_doc_quoted: false,
1715 here_string_content: None,
1716 }]),
1717 );
1718
1719 let ast = Ast::FunctionCall {
1721 name: "test_func".to_string(),
1722 args: vec!["hello".to_string()],
1723 };
1724 let exit_code = execute(ast, &mut shell_state);
1725 assert_eq!(exit_code, 0);
1726 }
1727
1728 #[test]
1729 fn test_execute_nonexistent_function() {
1730 let mut shell_state = ShellState::new();
1731 let ast = Ast::FunctionCall {
1732 name: "nonexistent".to_string(),
1733 args: vec![],
1734 };
1735 let exit_code = execute(ast, &mut shell_state);
1736 assert_eq!(exit_code, 1); }
1738
1739 #[test]
1740 fn test_execute_function_integration() {
1741 let mut shell_state = ShellState::new();
1743
1744 let define_ast = Ast::FunctionDefinition {
1746 name: "hello".to_string(),
1747 body: Box::new(Ast::Pipeline(vec![ShellCommand {
1748 args: vec!["printf".to_string(), "Hello from function".to_string()],
1749 input: None,
1750 output: None,
1751 append: None,
1752 here_doc_delimiter: None,
1753 here_doc_quoted: false,
1754 here_string_content: None,
1755 }])),
1756 };
1757 let exit_code = execute(define_ast, &mut shell_state);
1758 assert_eq!(exit_code, 0);
1759
1760 let call_ast = Ast::FunctionCall {
1762 name: "hello".to_string(),
1763 args: vec![],
1764 };
1765 let exit_code = execute(call_ast, &mut shell_state);
1766 assert_eq!(exit_code, 0);
1767 }
1768
1769 #[test]
1770 fn test_execute_function_with_local_variables() {
1771 let mut shell_state = ShellState::new();
1772
1773 shell_state.set_var("global_var", "global_value".to_string());
1775
1776 let define_ast = Ast::FunctionDefinition {
1778 name: "test_func".to_string(),
1779 body: Box::new(Ast::Sequence(vec![
1780 Ast::LocalAssignment {
1781 var: "local_var".to_string(),
1782 value: "local_value".to_string(),
1783 },
1784 Ast::Assignment {
1785 var: "global_var".to_string(),
1786 value: "modified_in_function".to_string(),
1787 },
1788 Ast::Pipeline(vec![ShellCommand {
1789 args: vec!["printf".to_string(), "success".to_string()],
1790 input: None,
1791 output: None,
1792 append: None,
1793 here_doc_delimiter: None,
1794 here_doc_quoted: false,
1795 here_string_content: None,
1796 }]),
1797 ])),
1798 };
1799 let exit_code = execute(define_ast, &mut shell_state);
1800 assert_eq!(exit_code, 0);
1801
1802 assert_eq!(
1804 shell_state.get_var("global_var"),
1805 Some("global_value".to_string())
1806 );
1807
1808 let call_ast = Ast::FunctionCall {
1810 name: "test_func".to_string(),
1811 args: vec![],
1812 };
1813 let exit_code = execute(call_ast, &mut shell_state);
1814 assert_eq!(exit_code, 0);
1815
1816 assert_eq!(
1818 shell_state.get_var("global_var"),
1819 Some("modified_in_function".to_string())
1820 );
1821 }
1822
1823 #[test]
1824 fn test_execute_nested_function_calls() {
1825 let mut shell_state = ShellState::new();
1826
1827 shell_state.set_var("global_var", "global".to_string());
1829
1830 let outer_func = Ast::FunctionDefinition {
1832 name: "outer".to_string(),
1833 body: Box::new(Ast::Sequence(vec![
1834 Ast::Assignment {
1835 var: "global_var".to_string(),
1836 value: "outer_modified".to_string(),
1837 },
1838 Ast::FunctionCall {
1839 name: "inner".to_string(),
1840 args: vec![],
1841 },
1842 Ast::Pipeline(vec![ShellCommand {
1843 args: vec!["printf".to_string(), "outer_done".to_string()],
1844 input: None,
1845 output: None,
1846 append: None,
1847 here_doc_delimiter: None,
1848 here_doc_quoted: false,
1849 here_string_content: None,
1850 }]),
1851 ])),
1852 };
1853
1854 let inner_func = Ast::FunctionDefinition {
1856 name: "inner".to_string(),
1857 body: Box::new(Ast::Sequence(vec![
1858 Ast::Assignment {
1859 var: "global_var".to_string(),
1860 value: "inner_modified".to_string(),
1861 },
1862 Ast::Pipeline(vec![ShellCommand {
1863 args: vec!["printf".to_string(), "inner_done".to_string()],
1864 input: None,
1865 output: None,
1866 append: None,
1867 here_doc_delimiter: None,
1868 here_doc_quoted: false,
1869 here_string_content: None,
1870 }]),
1871 ])),
1872 };
1873
1874 execute(outer_func, &mut shell_state);
1876 execute(inner_func, &mut shell_state);
1877
1878 shell_state.set_var("global_var", "initial".to_string());
1880
1881 let call_ast = Ast::FunctionCall {
1883 name: "outer".to_string(),
1884 args: vec![],
1885 };
1886 let exit_code = execute(call_ast, &mut shell_state);
1887 assert_eq!(exit_code, 0);
1888
1889 assert_eq!(
1892 shell_state.get_var("global_var"),
1893 Some("inner_modified".to_string())
1894 );
1895 }
1896
1897 #[test]
1898 fn test_here_string_execution() {
1899 let cmd = ShellCommand {
1901 args: vec!["cat".to_string()],
1902 input: None,
1903 output: None,
1904 append: None,
1905 here_doc_delimiter: None,
1906 here_doc_quoted: false,
1907 here_string_content: Some("hello world".to_string()),
1908 };
1909
1910 assert_eq!(cmd.args, vec!["cat"]);
1913 assert_eq!(cmd.here_string_content, Some("hello world".to_string()));
1914 }
1915
1916 #[test]
1917 fn test_here_document_execution() {
1918 let cmd = ShellCommand {
1920 args: vec!["cat".to_string()],
1921 input: None,
1922 output: None,
1923 append: None,
1924 here_doc_delimiter: Some("EOF".to_string()),
1925 here_doc_quoted: false,
1926 here_string_content: None,
1927 };
1928
1929 assert_eq!(cmd.args, vec!["cat"]);
1932 assert_eq!(cmd.here_doc_delimiter, Some("EOF".to_string()));
1933 }
1934
1935 #[test]
1936 fn test_here_document_with_variable_expansion() {
1937 let mut shell_state = ShellState::new();
1939 shell_state.set_var("PWD", "/test/path".to_string());
1940
1941 let content = "Working dir: $PWD";
1943 let expanded = expand_variables_in_string(content, &mut shell_state);
1944
1945 assert_eq!(expanded, "Working dir: /test/path");
1946 }
1947
1948 #[test]
1949 fn test_here_document_with_command_substitution_builtin() {
1950 let mut shell_state = ShellState::new();
1952 shell_state.set_var("PWD", "/test/dir".to_string());
1953
1954 let content = "Current directory: `pwd`";
1956 let expanded = expand_variables_in_string(content, &mut shell_state);
1957
1958 assert!(expanded.contains("Current directory: "));
1960 }
1961}