Rust · 15811 bytes Raw Blame History
1 use rush_expand::Context;
2 use rush_parser::ast::{CommandType, Redirect};
3 use std::fs::{File, OpenOptions};
4 use std::io;
5 use std::process::{Command, Stdio};
6 use thiserror::Error;
7
8 #[derive(Error, Debug)]
9 pub enum RedirectError {
10 #[error("Failed to open file: {0}")]
11 FileOpenError(#[from] io::Error),
12
13 #[error("Invalid file descriptor: {0}")]
14 InvalidFileDescriptor(u32),
15
16 #[error("Expansion error: {0}")]
17 ExpansionError(String),
18 }
19
20 /// Apply redirections to a Command
21 ///
22 /// This function processes all redirections and configures the Command's stdio accordingly.
23 /// Returns optional stdin content for heredocs/herestrings.
24 pub fn apply_redirects(
25 cmd: &mut Command,
26 redirects: &[Redirect],
27 context: &mut Context,
28 ) -> Result<Option<String>, RedirectError> {
29 let mut stdin_content = None;
30 for redirect in redirects {
31 if let Some(content) = apply_single_redirect(cmd, redirect, context)? {
32 stdin_content = Some(content);
33 }
34 }
35 Ok(stdin_content)
36 }
37
38 fn apply_single_redirect(
39 cmd: &mut Command,
40 redirect: &Redirect,
41 context: &mut Context,
42 ) -> Result<Option<String>, RedirectError> {
43 match redirect {
44 Redirect::Input { file } => {
45 // Expand the filename
46 let filename = rush_expand::expand_word(file, context)
47 .map_err(|e| RedirectError::ExpansionError(e.to_string()))?;
48
49 // Open file for reading
50 let file_handle = File::open(&filename)?;
51 cmd.stdin(file_handle);
52 Ok(None)
53 }
54
55 Redirect::Output { fd, file } => {
56 // Expand the filename
57 let filename = rush_expand::expand_word(file, context)
58 .map_err(|e| RedirectError::ExpansionError(e.to_string()))?;
59
60 // Open file for writing (truncate)
61 let file_handle = OpenOptions::new()
62 .write(true)
63 .create(true)
64 .truncate(true)
65 .open(&filename)?;
66
67 match fd {
68 None | Some(1) => {
69 // Redirect stdout
70 cmd.stdout(file_handle);
71 }
72 Some(2) => {
73 // Redirect stderr
74 cmd.stderr(file_handle);
75 }
76 Some(fd_num) => {
77 return Err(RedirectError::InvalidFileDescriptor(*fd_num));
78 }
79 }
80 Ok(None)
81 }
82
83 Redirect::OutputAppend { fd, file } => {
84 // Expand the filename
85 let filename = rush_expand::expand_word(file, context)
86 .map_err(|e| RedirectError::ExpansionError(e.to_string()))?;
87
88 // Open file for appending
89 let file_handle = OpenOptions::new()
90 .write(true)
91 .create(true)
92 .append(true)
93 .open(&filename)?;
94
95 match fd {
96 None | Some(1) => {
97 // Redirect stdout
98 cmd.stdout(file_handle);
99 }
100 Some(2) => {
101 // Redirect stderr
102 cmd.stderr(file_handle);
103 }
104 Some(fd_num) => {
105 return Err(RedirectError::InvalidFileDescriptor(*fd_num));
106 }
107 }
108 Ok(None)
109 }
110
111 Redirect::StderrToStdout => {
112 // Redirect stderr to stdout
113 // Note: This is a simplified version. Proper implementation requires
114 // using unsafe dup2 system calls to redirect fd 2 to fd 1
115 cmd.stderr(Stdio::inherit());
116 Ok(None)
117 }
118
119 Redirect::AllOutput { file, append } => {
120 // Expand the filename
121 let filename = rush_expand::expand_word(file, context)
122 .map_err(|e| RedirectError::ExpansionError(e.to_string()))?;
123
124 // Open file
125 let file_handle = if *append {
126 OpenOptions::new()
127 .write(true)
128 .create(true)
129 .append(true)
130 .open(&filename)?
131 } else {
132 OpenOptions::new()
133 .write(true)
134 .create(true)
135 .truncate(true)
136 .open(&filename)?
137 };
138
139 // Redirect both stdout and stderr
140 // Note: This requires duplicating the file handle
141 cmd.stdout(file_handle.try_clone()?);
142 cmd.stderr(file_handle);
143 Ok(None)
144 }
145
146 Redirect::Heredoc { delimiter: _, content, strip_tabs, expand } => {
147 // Process heredoc content
148 let mut lines = content.clone();
149
150 // Strip leading tabs if requested
151 if *strip_tabs {
152 lines = lines.iter()
153 .map(|line| line.trim_start_matches('\t'))
154 .map(|s| s.to_string())
155 .collect();
156 }
157
158 // Expand variables if requested
159 if *expand {
160 let expanded_lines: Result<Vec<String>, _> = lines.iter()
161 .map(|line| {
162 // Parse the line to detect variables and other expansions
163 // We need to parse it as a simple command and extract the word
164 let fake_cmd = format!("echo {}", line);
165 match rush_parser::parse_line(&fake_cmd) {
166 Ok(rush_parser::Statement::Complete(cmd)) => {
167 match &cmd.command {
168 CommandType::Simple(simple) => {
169 // Get the words (skip "echo")
170 if simple.words.len() > 1 {
171 rush_expand::expand_words(&simple.words[1..], context)
172 .map(|words| words.join(" "))
173 .map_err(|e| RedirectError::ExpansionError(e.to_string()))
174 } else {
175 Ok(line.clone())
176 }
177 }
178 _ => Ok(line.clone()),
179 }
180 }
181 _ => Ok(line.clone()),
182 }
183 })
184 .collect();
185 lines = expanded_lines?;
186 }
187
188 // Join lines and return content
189 let input = lines.join("\n");
190 cmd.stdin(Stdio::piped());
191 Ok(Some(input))
192 }
193
194 Redirect::Herestring { content } => {
195 // Expand the content word
196 let mut input = rush_expand::expand_word(content, context)
197 .map_err(|e| RedirectError::ExpansionError(e.to_string()))?;
198
199 // Herestrings add a trailing newline
200 input.push('\n');
201 cmd.stdin(Stdio::piped());
202 Ok(Some(input))
203 }
204
205 Redirect::ProcessSubstInput { command } => {
206 // Process substitution: <(command)
207 // Creates a FIFO that provides the command's stdout
208 #[cfg(unix)]
209 {
210 let fifo_path = crate::process_subst::execute_process_subst_input(command, context)
211 .map_err(|e| RedirectError::FileOpenError(std::io::Error::new(
212 std::io::ErrorKind::Other,
213 format!("Process substitution failed: {}", e),
214 )))?;
215
216 // Open the FIFO for reading (this will block until the writer opens it)
217 let file_handle = File::open(&fifo_path)?;
218 cmd.stdin(file_handle);
219 Ok(None)
220 }
221 #[cfg(not(unix))]
222 {
223 Err(RedirectError::FileOpenError(std::io::Error::new(
224 std::io::ErrorKind::Unsupported,
225 "Process substitution not supported on this platform".to_string(),
226 )))
227 }
228 }
229
230 Redirect::ProcessSubstOutput { command } => {
231 // Process substitution: >(command)
232 // Creates a FIFO that feeds into the command's stdin
233 #[cfg(unix)]
234 {
235 let fifo_path = crate::process_subst::execute_process_subst_output(command, context)
236 .map_err(|e| RedirectError::FileOpenError(std::io::Error::new(
237 std::io::ErrorKind::Other,
238 format!("Process substitution failed: {}", e),
239 )))?;
240
241 // Open the FIFO for writing (this will block until the reader opens it)
242 let file_handle = OpenOptions::new()
243 .write(true)
244 .open(&fifo_path)?;
245 cmd.stdout(file_handle);
246 Ok(None)
247 }
248 #[cfg(not(unix))]
249 {
250 Err(RedirectError::FileOpenError(std::io::Error::new(
251 std::io::ErrorKind::Unsupported,
252 "Process substitution not supported on this platform".to_string(),
253 )))
254 }
255 }
256 }
257 }
258
259 /// Saved file descriptors for restoring after builtin execution
260 #[cfg(unix)]
261 pub struct SavedFds {
262 saved_stdout: Option<i32>,
263 saved_stderr: Option<i32>,
264 }
265
266 #[cfg(unix)]
267 impl SavedFds {
268 fn new() -> Self {
269 Self {
270 saved_stdout: None,
271 saved_stderr: None,
272 }
273 }
274 }
275
276 #[cfg(unix)]
277 impl Drop for SavedFds {
278 fn drop(&mut self) {
279 use nix::libc;
280 // Restore stdout if saved
281 if let Some(saved) = self.saved_stdout {
282 unsafe {
283 libc::dup2(saved, libc::STDOUT_FILENO);
284 libc::close(saved);
285 }
286 }
287 // Restore stderr if saved
288 if let Some(saved) = self.saved_stderr {
289 unsafe {
290 libc::dup2(saved, libc::STDERR_FILENO);
291 libc::close(saved);
292 }
293 }
294 }
295 }
296
297 /// Apply redirections to the current process (for builtins)
298 /// Returns a guard that restores the original file descriptors when dropped
299 #[cfg(unix)]
300 pub fn apply_redirects_to_process(
301 redirects: &[Redirect],
302 context: &mut Context,
303 ) -> Result<SavedFds, RedirectError> {
304 use nix::libc;
305 use std::os::unix::io::IntoRawFd;
306
307 let mut saved = SavedFds::new();
308
309 for redirect in redirects {
310 match redirect {
311 Redirect::Output { fd, file } => {
312 let filename = rush_expand::expand_word(file, context)
313 .map_err(|e| RedirectError::ExpansionError(e.to_string()))?;
314
315 let file_handle = OpenOptions::new()
316 .write(true)
317 .create(true)
318 .truncate(true)
319 .open(&filename)?;
320
321 let file_fd = file_handle.into_raw_fd();
322
323 match fd {
324 None | Some(1) => {
325 // Save stdout if not already saved
326 if saved.saved_stdout.is_none() {
327 saved.saved_stdout = Some(unsafe { libc::dup(libc::STDOUT_FILENO) });
328 }
329 unsafe { libc::dup2(file_fd, libc::STDOUT_FILENO); }
330 unsafe { libc::close(file_fd); }
331 }
332 Some(2) => {
333 // Save stderr if not already saved
334 if saved.saved_stderr.is_none() {
335 saved.saved_stderr = Some(unsafe { libc::dup(libc::STDERR_FILENO) });
336 }
337 unsafe { libc::dup2(file_fd, libc::STDERR_FILENO); }
338 unsafe { libc::close(file_fd); }
339 }
340 Some(fd_num) => {
341 unsafe { libc::close(file_fd); }
342 return Err(RedirectError::InvalidFileDescriptor(*fd_num));
343 }
344 }
345 }
346 Redirect::OutputAppend { fd, file } => {
347 let filename = rush_expand::expand_word(file, context)
348 .map_err(|e| RedirectError::ExpansionError(e.to_string()))?;
349
350 let file_handle = OpenOptions::new()
351 .write(true)
352 .create(true)
353 .append(true)
354 .open(&filename)?;
355
356 let file_fd = file_handle.into_raw_fd();
357
358 match fd {
359 None | Some(1) => {
360 if saved.saved_stdout.is_none() {
361 saved.saved_stdout = Some(unsafe { libc::dup(libc::STDOUT_FILENO) });
362 }
363 unsafe { libc::dup2(file_fd, libc::STDOUT_FILENO); }
364 unsafe { libc::close(file_fd); }
365 }
366 Some(2) => {
367 if saved.saved_stderr.is_none() {
368 saved.saved_stderr = Some(unsafe { libc::dup(libc::STDERR_FILENO) });
369 }
370 unsafe { libc::dup2(file_fd, libc::STDERR_FILENO); }
371 unsafe { libc::close(file_fd); }
372 }
373 Some(fd_num) => {
374 unsafe { libc::close(file_fd); }
375 return Err(RedirectError::InvalidFileDescriptor(*fd_num));
376 }
377 }
378 }
379 Redirect::StderrToStdout => {
380 // 2>&1 - redirect stderr to stdout
381 if saved.saved_stderr.is_none() {
382 saved.saved_stderr = Some(unsafe { libc::dup(libc::STDERR_FILENO) });
383 }
384 unsafe { libc::dup2(libc::STDOUT_FILENO, libc::STDERR_FILENO); }
385 }
386 Redirect::AllOutput { file, append } => {
387 let filename = rush_expand::expand_word(file, context)
388 .map_err(|e| RedirectError::ExpansionError(e.to_string()))?;
389
390 let file_handle = if *append {
391 OpenOptions::new()
392 .write(true)
393 .create(true)
394 .append(true)
395 .open(&filename)?
396 } else {
397 OpenOptions::new()
398 .write(true)
399 .create(true)
400 .truncate(true)
401 .open(&filename)?
402 };
403
404 let file_fd = file_handle.into_raw_fd();
405
406 if saved.saved_stdout.is_none() {
407 saved.saved_stdout = Some(unsafe { libc::dup(libc::STDOUT_FILENO) });
408 }
409 if saved.saved_stderr.is_none() {
410 saved.saved_stderr = Some(unsafe { libc::dup(libc::STDERR_FILENO) });
411 }
412 unsafe {
413 libc::dup2(file_fd, libc::STDOUT_FILENO);
414 libc::dup2(file_fd, libc::STDERR_FILENO);
415 libc::close(file_fd);
416 }
417 }
418 // Input redirects, heredocs, etc. are not commonly used with builtins
419 // They're handled specially or not applicable
420 _ => {}
421 }
422 }
423
424 Ok(saved)
425 }
426
427 /// Stub for non-Unix platforms
428 #[cfg(not(unix))]
429 pub struct SavedFds;
430
431 #[cfg(not(unix))]
432 pub fn apply_redirects_to_process(
433 _redirects: &[Redirect],
434 _context: &mut Context,
435 ) -> Result<SavedFds, RedirectError> {
436 Ok(SavedFds)
437 }
438