From d22732d1c3f4629f81f8fe0212dcc06df40be81f Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Wed, 5 Jan 2022 16:40:58 -0500 Subject: basic input/output redirection --- src/parse.rs | 72 ++++++++++++++++++++++++++++++++++- src/pipeline/command.rs | 99 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 158 insertions(+), 13 deletions(-) diff --git a/src/parse.rs b/src/parse.rs index 45c83c5..9abfaac 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -4,10 +4,60 @@ use pest::Parser as _; #[grammar = "shell.pest"] struct Shell; +#[derive(Debug, Clone)] +pub enum RedirectTarget { + Fd(std::os::unix::io::RawFd), + File(std::path::PathBuf), +} + +#[derive(Debug, Clone)] +pub enum Direction { + In, + Out, +} + +impl Direction { + fn parse(c: u8) -> Option { + Some(match c { + b'>' => Self::Out, + b'<' => Self::In, + _ => return None, + }) + } +} + +#[derive(Debug, Clone)] +pub struct Redirect { + pub from: std::os::unix::io::RawFd, + pub to: RedirectTarget, + pub dir: Direction, +} + +impl Redirect { + fn parse(s: &str) -> Self { + let (from, to) = s.split_once(&['<', '>'][..]).unwrap(); + let dir = Direction::parse(s.as_bytes()[from.len()]).unwrap(); + let from = if from.is_empty() { + match dir { + Direction::In => 0, + Direction::Out => 1, + } + } else { + from.parse().unwrap() + }; + let to = to.strip_prefix('&').map_or_else( + || RedirectTarget::File(to.into()), + |fd| RedirectTarget::Fd(fd.parse().unwrap()), + ); + Self { from, to, dir } + } +} + #[derive(Debug)] pub struct Word { word: String, interpolate: bool, + quoted: bool, } impl Word { @@ -24,6 +74,10 @@ impl Word { word.as_rule(), Rule::bareword | Rule::double_string ), + quoted: matches!( + word.as_rule(), + Rule::single_string | Rule::double_string + ), } } } @@ -32,6 +86,7 @@ impl Word { pub struct Exe { exe: Word, args: Vec, + redirects: Vec, } impl Exe { @@ -39,8 +94,17 @@ impl Exe { assert!(matches!(pair.as_rule(), Rule::exe)); let mut iter = pair.into_inner(); let exe = Word::build_ast(iter.next().unwrap()); - let args = iter.map(Word::build_ast).collect(); - Self { exe, args } + let (args, redirects): (_, Vec<_>) = + iter.map(Word::build_ast).partition(|word| { + word.quoted || !word.word.contains(&['<', '>'][..]) + }); + let redirects = + redirects.iter().map(|r| Redirect::parse(&r.word)).collect(); + Self { + exe, + args, + redirects, + } } pub fn exe(&self) -> &str { @@ -51,6 +115,10 @@ impl Exe { self.args.iter().map(|arg| arg.word.as_ref()) } + pub fn redirects(&self) -> &[Redirect] { + &self.redirects + } + pub fn shift(&mut self) { self.exe = self.args.remove(0); } diff --git a/src/pipeline/command.rs b/src/pipeline/command.rs index a6b363a..3a898d1 100644 --- a/src/pipeline/command.rs +++ b/src/pipeline/command.rs @@ -3,6 +3,10 @@ use crate::pipeline::prelude::*; pub struct Command { inner: Inner, exe: String, + redirects: Vec, + pre_exec: Option< + Box std::io::Result<()> + Send + Sync + 'static>, + >, } pub enum Inner { Binary(async_std::process::Command), @@ -12,32 +16,41 @@ pub enum Inner { impl Command { pub fn new(exe: crate::parse::Exe) -> Self { let exe_str = exe.exe().to_string(); + let redirects = exe.redirects().to_vec(); Self { inner: super::builtins::Command::new(exe).map_or_else( |exe| Self::new_binary(exe).inner, Inner::Builtin, ), exe: exe_str, + redirects, + pre_exec: None, } } #[allow(clippy::needless_pass_by_value)] pub fn new_binary(exe: crate::parse::Exe) -> Self { let exe_str = exe.exe().to_string(); + let redirects = exe.redirects().to_vec(); let mut cmd = async_std::process::Command::new(exe.exe()); cmd.args(exe.args()); Self { inner: Inner::Binary(cmd), exe: exe_str, + redirects, + pre_exec: None, } } pub fn new_builtin(exe: crate::parse::Exe) -> Self { let exe_str = exe.exe().to_string(); + let redirects = exe.redirects().to_vec(); Self { inner: super::builtins::Command::new(exe) .map_or_else(|_| todo!(), Inner::Builtin), exe: exe_str, + redirects, + pre_exec: None, } } @@ -80,28 +93,51 @@ impl Command { where F: 'static + FnMut() -> std::io::Result<()> + Send + Sync, { - match &mut self.inner { - Inner::Binary(cmd) => { - cmd.pre_exec(f); - } - Inner::Builtin(cmd) => { - cmd.pre_exec(f); - } - } + self.pre_exec = Some(Box::new(f)); } pub fn spawn(self, env: &Env) -> anyhow::Result { - match self.inner { + let Self { + inner, + exe, + redirects, + pre_exec, + } = self; + + #[allow(clippy::as_conversions)] + let pre_exec = pre_exec.map_or_else( + || { + let redirects = redirects.clone(); + Box::new(move || { + apply_redirects(&redirects)?; + Ok(()) + }) + as Box std::io::Result<()> + Send + Sync> + }, + |mut pre_exec| { + let redirects = redirects.clone(); + Box::new(move || { + apply_redirects(&redirects)?; + pre_exec()?; + Ok(()) + }) + }, + ); + match inner { Inner::Binary(mut cmd) => { + unsafe { cmd.pre_exec(pre_exec) }; Ok(Child::Binary(cmd.spawn().map_err(|e| { anyhow::anyhow!( "{}: {}", crate::format::io_error(&e), - self.exe + exe ) })?)) } - Inner::Builtin(cmd) => Ok(Child::Builtin(cmd.spawn(env)?)), + Inner::Builtin(mut cmd) => { + unsafe { cmd.pre_exec(pre_exec) }; + Ok(Child::Builtin(cmd.spawn(env)?)) + } } } } @@ -138,3 +174,44 @@ impl<'a> Child<'a> { }) } } + +fn apply_redirects( + redirects: &[crate::parse::Redirect], +) -> std::io::Result<()> { + for redirect in redirects { + match &redirect.to { + crate::parse::RedirectTarget::Fd(fd) => { + nix::unistd::dup2(*fd, redirect.from)?; + } + crate::parse::RedirectTarget::File(path) => { + use nix::fcntl::OFlag; + use nix::sys::stat::Mode; + let fd = match redirect.dir { + crate::parse::Direction::In => nix::fcntl::open( + path, + OFlag::O_NOCTTY | OFlag::O_RDONLY, + Mode::empty(), + )?, + crate::parse::Direction::Out => nix::fcntl::open( + path, + OFlag::O_CREAT + | OFlag::O_NOCTTY + | OFlag::O_WRONLY + | OFlag::O_TRUNC, + Mode::S_IRUSR + | Mode::S_IWUSR + | Mode::S_IRGRP + | Mode::S_IWGRP + | Mode::S_IROTH + | Mode::S_IWOTH, + )?, + }; + if fd != redirect.from { + nix::unistd::dup2(fd, redirect.from)?; + nix::unistd::close(fd)?; + } + } + } + } + Ok(()) +} -- cgit v1.2.3-54-g00ecf