Rust · 5675 bytes Raw Blame History
1 //! Internal command substitution execution
2 //!
3 //! This module provides the ability to execute commands internally and capture their stdout,
4 //! enabling $(command) substitution without relying on external shells.
5
6 use rush_expand::Context;
7
8 /// Execute a command string internally and capture its stdout
9 ///
10 /// This is the callback function that gets registered with Context.command_executor
11 /// to enable internal command substitution.
12 ///
13 /// The function:
14 /// 1. Parses the command string using rush-parser
15 /// 2. Forks a child process
16 /// 3. The child executes the command with stdout redirected to a pipe
17 /// 4. The parent reads and returns the captured output
18 pub fn execute_for_substitution(cmd: &str, context: &mut Context) -> Result<String, String> {
19 // Parse the command
20 let statement = rush_parser::parse_line(cmd)
21 .map_err(|e| format!("Parse error: {}", e))?;
22
23 // Use fork + pipe to capture stdout (like bash does)
24 #[cfg(unix)]
25 {
26 execute_with_fork_capture(statement, context)
27 }
28
29 #[cfg(not(unix))]
30 {
31 // On non-Unix platforms, fall back to sh -c for now
32 // TODO: Implement Windows-specific stdout capture
33 Err("Command substitution on non-Unix platforms not yet supported".to_string())
34 }
35 }
36
37 #[cfg(unix)]
38 fn execute_with_fork_capture(
39 statement: rush_parser::Statement,
40 context: &mut Context,
41 ) -> Result<String, String> {
42 use nix::libc;
43 use nix::unistd::{pipe, dup2, fork, ForkResult};
44 use nix::sys::wait::{waitpid, WaitStatus};
45 use std::os::unix::io::{RawFd, FromRawFd, IntoRawFd};
46 use std::io::{Read, Write};
47
48 const STDOUT_FD: RawFd = 1;
49
50 // Create a pipe for capturing output
51 let (read_fd, write_fd) = pipe().map_err(|e| format!("Failed to create pipe: {}", e))?;
52 let read_raw_fd = read_fd.into_raw_fd();
53 let write_raw_fd = write_fd.into_raw_fd();
54
55 // Fork to execute the command in a subprocess
56 match unsafe { fork() } {
57 Ok(ForkResult::Child) => {
58 // Child process: redirect stdout to pipe and execute
59
60 // Close the read end of the pipe (child only writes)
61 unsafe { libc::close(read_raw_fd); }
62
63 // Redirect stdout to the write end of the pipe
64 if let Err(_) = dup2(write_raw_fd, STDOUT_FD) {
65 std::process::exit(1);
66 }
67
68 // Close the original write_fd (now that stdout points to it)
69 unsafe { libc::close(write_raw_fd); }
70
71 // Execute the command
72 let exit_code = match statement {
73 rush_parser::Statement::Complete(cmd) => {
74 match crate::control_flow::execute_complete_command(&cmd, context) {
75 Ok(result) => result.exit_code(),
76 Err(_) => 1,
77 }
78 }
79 rush_parser::Statement::Script(commands) => {
80 let mut last_code = 0;
81 for cmd in &commands {
82 match crate::control_flow::execute_complete_command(cmd, context) {
83 Ok(result) => last_code = result.exit_code(),
84 Err(_) => {
85 last_code = 1;
86 break;
87 }
88 }
89 }
90 last_code
91 }
92 rush_parser::Statement::Empty => 0,
93 };
94
95 // Ensure all output is flushed before exiting
96 let _ = std::io::stdout().flush();
97
98 // Exit the child process
99 std::process::exit(exit_code);
100 }
101 Ok(ForkResult::Parent { child }) => {
102 // Parent process: close write end and read from pipe
103
104 // Close the write end (parent only reads)
105 unsafe { libc::close(write_raw_fd); }
106
107 // Read all output from the pipe
108 let mut output = String::new();
109 let mut reader = unsafe { std::fs::File::from_raw_fd(read_raw_fd) };
110 if let Err(e) = reader.read_to_string(&mut output) {
111 return Err(format!("Failed to read output: {}", e));
112 }
113
114 // Wait for child to complete
115 match waitpid(child, None) {
116 Ok(WaitStatus::Exited(_, code)) => {
117 context.set_exit_status(code);
118 }
119 Ok(_) => {
120 context.set_exit_status(0);
121 }
122 Err(e) => {
123 return Err(format!("Failed to wait for child: {}", e));
124 }
125 }
126
127 // Bash behavior: trim trailing newlines from command substitution
128 while output.ends_with('\n') {
129 output.pop();
130 }
131
132 Ok(output)
133 }
134 Err(e) => {
135 // Fork failed, clean up
136 unsafe {
137 libc::close(read_raw_fd);
138 libc::close(write_raw_fd);
139 }
140 Err(format!("Failed to fork: {}", e))
141 }
142 }
143 }
144
145 #[cfg(test)]
146 mod tests {
147 use super::*;
148
149 #[test]
150 #[cfg(unix)]
151 fn test_internal_echo() {
152 let mut context = Context::empty();
153 let result = execute_for_substitution("echo hello", &mut context).unwrap();
154 assert_eq!(result, "hello");
155 }
156
157 #[test]
158 #[cfg(unix)]
159 fn test_internal_with_variables() {
160 let mut context = Context::empty();
161 context.set_var("x", "world").unwrap();
162 let result = execute_for_substitution("echo hello $x", &mut context).unwrap();
163 assert_eq!(result, "hello world");
164 }
165 }
166