use super::*;
pub(super) struct SavedFd {
target: RedirectTarget,
previous_fd: sys::FileDescriptor,
}
pub(super) struct RedirectGuard {
saved: Vec<SavedFd>,
}
impl RedirectGuard {
pub(super) fn apply<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
redirects: &[IoRedirect],
) -> Result<Self, i32> {
let mut saved = Vec::new();
for redir in redirects {
match apply_one_redirect(state, runtime, redir) {
Some(entry) => saved.push(entry),
None => {
restore_saved_redirects(state, saved);
return Err(1);
}
}
}
Ok(Self { saved })
}
pub(super) fn restore(self, state: &mut ShellState) {
restore_saved_redirects(state, self.saved);
}
}
#[derive(Clone, Copy)]
enum RedirectTarget {
Stdin,
Stdout,
Stderr,
Raw(sys::FileDescriptor),
}
fn redirect_target_for_fd(fd: i32) -> RedirectTarget {
match fd {
0 => RedirectTarget::Stdin,
1 => RedirectTarget::Stdout,
2 => RedirectTarget::Stderr,
_ => RedirectTarget::Raw(sys::FileDescriptor::from(fd)),
}
}
fn target_fd_for_redirect(redir: &IoRedirect) -> RedirectTarget {
if let Some(n) = redir.io_number {
return redirect_target_for_fd(n as i32);
}
match redir.op {
IoRedirectOp::Less
| IoRedirectOp::LessGreat
| IoRedirectOp::DLess
| IoRedirectOp::DLessDash => RedirectTarget::Stdin,
_ => RedirectTarget::Stdout,
}
}
fn current_redirect_fd(state: &ShellState, target: RedirectTarget) -> sys::FileDescriptor {
match target {
RedirectTarget::Stdin => state.stdin_fd,
RedirectTarget::Stdout => state.stdout_fd,
RedirectTarget::Stderr => state.stderr_fd,
RedirectTarget::Raw(fd) => state
.raw_fds
.get(&fd.as_i32())
.copied()
.unwrap_or(sys::FileDescriptor::INVALID),
}
}
fn set_redirect_fd(state: &mut ShellState, target: RedirectTarget, fd: sys::FileDescriptor) {
match target {
RedirectTarget::Stdin => state.stdin_fd = fd,
RedirectTarget::Stdout => state.stdout_fd = fd,
RedirectTarget::Stderr => state.stderr_fd = fd,
RedirectTarget::Raw(target_fd) => {
let key = target_fd.as_i32();
if !fd.is_valid() {
state.raw_fds.remove(&key);
} else {
state.raw_fds.insert(key, fd);
}
}
}
}
fn close_redirect_fd_if_replaced(
state: &ShellState,
target: RedirectTarget,
restore_fd: sys::FileDescriptor,
) {
let current = current_redirect_fd(state, target);
if current.is_valid() && current != restore_fd {
current.close();
}
}
fn duplicate_redirect_source_fd(
state: &ShellState,
fd: i32,
) -> Result<sys::FileDescriptor, std::io::Error> {
let source = current_redirect_fd(state, redirect_target_for_fd(fd));
if !source.is_valid() {
return Err(std::io::Error::from_raw_os_error(libc::EBADF));
}
source.dup()
}
fn open_redirect_file(
state: &ShellState,
filename: &str,
opened: io::Result<fs::File>,
) -> Option<sys::FileDescriptor> {
match opened {
Ok(file) => Some(sys::FileDescriptor::from(file.into_raw_fd())),
Err(err) => {
shell_errln(state, &format!("{filename}: {err}"));
None
}
}
}
fn assign_variable(
state: &mut ShellState,
name: &str,
value: String,
attrib: u32,
context: &str,
) -> Result<(), i32> {
if state.env_set(name, value, attrib) {
Ok(())
} else {
let message = if context.is_empty() {
format!("{name}: readonly variable")
} else {
format!("{context}: {name}: readonly variable")
};
shell_errln(state, &message);
Err(1)
}
}
fn apply_one_redirect<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
redir: &IoRedirect,
) -> Option<SavedFd> {
let target = target_fd_for_redirect(redir);
let previous_fd = current_redirect_fd(state, target);
let filename = shell_expand::expand_word_nosplit(state, runtime, &redir.name);
if state.take_expansion_error().is_some() {
return None;
}
let filename_path = resolve_path(&state.cwd, Path::new(&filename));
let new_fd = match redir.op {
IoRedirectOp::Less => open_redirect_file(state, &filename, fs::File::open(&filename_path))?,
IoRedirectOp::Great => {
let opened = if state.has_option(OPT_NOCLOBBER) {
OpenOptions::new()
.write(true)
.create_new(true)
.open(&filename_path)
} else {
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&filename_path)
};
open_redirect_file(state, &filename, opened)?
}
IoRedirectOp::Clobber => open_redirect_file(
state,
&filename,
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&filename_path),
)?,
IoRedirectOp::DGreat => open_redirect_file(
state,
&filename,
OpenOptions::new()
.create(true)
.append(true)
.open(&filename_path),
)?,
IoRedirectOp::LessAnd | IoRedirectOp::GreatAnd => {
if filename == "-" {
set_redirect_fd(state, target, sys::FileDescriptor::INVALID);
return Some(SavedFd {
target,
previous_fd,
});
}
match filename.parse::<i32>() {
Ok(fd) => match duplicate_redirect_source_fd(state, fd) {
Ok(fd) => fd,
Err(_) => {
shell_errln(state, &format!("{filename}: bad file descriptor"));
return None;
}
},
Err(_) => {
shell_errln(state, &format!("{filename}: bad file descriptor"));
return None;
}
}
}
IoRedirectOp::LessGreat => open_redirect_file(
state,
&filename,
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&filename_path),
)?,
IoRedirectOp::DLess | IoRedirectOp::DLessDash => {
let mut content = String::new();
for w in &redir.here_document {
if redir.here_document_expand {
content.push_str(&shell_expand::expand_word_nosplit(state, runtime, w));
if state.take_expansion_error().is_some() {
return None;
}
} else {
content.push_str(&crate::ast::canonical_here_doc_literal_line(w));
}
content.push('\n');
}
if let Ok(p) = sys::OsPipe::new() {
if let Err(err) = p.write_fd.write_all(content.as_bytes()) {
shell_errln(state, &format!("here-document: {err}"));
p.write_fd.close();
p.read_fd.close();
return None;
}
p.write_fd.close();
p.read_fd
} else {
return None;
}
}
};
set_redirect_fd(state, target, new_fd);
Some(SavedFd {
target,
previous_fd,
})
}
fn restore_saved_redirects(state: &mut ShellState, saved: Vec<SavedFd>) {
for s in saved.into_iter().rev() {
close_redirect_fd_if_replaced(state, s.target, s.previous_fd);
set_redirect_fd(state, s.target, s.previous_fd);
}
}
pub(super) fn set_assignment_values<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
assignments: &[Assignment],
attrib: u32,
capture_old: bool,
context: &str,
) -> Result<Vec<(String, Option<Variable>)>, i32> {
let mut new_values = Vec::with_capacity(assignments.len());
for assign in assignments {
let value = shell_expand::expand_word_nosplit(state, runtime, &assign.value);
if let Some(status) = state.take_expansion_error() {
return Err(status);
}
new_values.push((
assign.name.clone(),
shell_expand::expand_tilde_assignment(state, &value),
));
}
let mut old_vars = Vec::new();
for (name, value) in new_values {
if capture_old {
old_vars.push((name.clone(), state.vars.get(&name).cloned()));
}
assign_variable(state, &name, value, attrib, context)?;
}
Ok(old_vars)
}
pub(super) fn restore_assignment_values(
state: &mut ShellState,
old_vars: Vec<(String, Option<Variable>)>,
) {
for (name, old) in old_vars {
if let Some(var) = old {
state.vars.insert(name, var);
} else {
state.vars.remove(&name);
}
}
}