diff --git a/src/lib/parser/pipelines/mod.rs b/src/lib/parser/pipelines/mod.rs index f48349e1010174dff60ed402928f1d42c0538299..7919b82f3f42a2a79f017230644754ccae4d3dcf 100644 --- a/src/lib/parser/pipelines/mod.rs +++ b/src/lib/parser/pipelines/mod.rs @@ -9,12 +9,7 @@ use crate::{ }; use itertools::Itertools; use small; -use std::{ - fmt, - fs::File, - io::{self, Write}, - os::unix::io::{FromRawFd, RawFd}, -}; +use std::fmt; #[derive(Debug, PartialEq, Clone, Copy)] pub enum RedirectFrom { @@ -42,52 +37,6 @@ pub enum Input { HereString(small::String), } -/// Create an OS pipe and write the contents of a byte slice to one end -/// such that reading from this pipe will produce the byte slice. Return -/// A file descriptor representing the read end of the pipe. -pub unsafe fn stdin_of<T: AsRef<[u8]>>(input: T) -> Result<RawFd, io::Error> { - let (reader, writer) = sys::pipe2(sys::O_CLOEXEC)?; - let mut infile = File::from_raw_fd(writer); - // Write the contents; make sure to use write_all so that we block until - // the entire string is written - infile.write_all(input.as_ref())?; - infile.flush()?; - // `infile` currently owns the writer end RawFd. If we just return the reader - // end and let `infile` go out of scope, it will be closed, sending EOF to - // the reader! - Ok(reader) -} - -impl Input { - pub fn get_infile(&mut self) -> Result<File, ()> { - match self { - Input::File(ref filename) => match File::open(filename.as_str()) { - Ok(file) => Ok(file), - Err(e) => { - eprintln!("ion: failed to redirect '{}' to stdin: {}", filename, e); - Err(()) - } - }, - Input::HereString(ref mut string) => { - if !string.ends_with('\n') { - string.push('\n'); - } - - match unsafe { stdin_of(&string) } { - Ok(stdio) => Ok(unsafe { File::from_raw_fd(stdio) }), - Err(e) => { - eprintln!( - "ion: failed to redirect herestring '{}' to stdin: {}", - string, e - ); - Err(()) - } - } - } - } - } -} - impl<'a> fmt::Display for Input { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { diff --git a/src/lib/shell/pipe_exec/mod.rs b/src/lib/shell/pipe_exec/mod.rs index b70f1f4b4577c8087b72256f40fc4e76ab14cd2e..fe9e31eecda9fbeb8cd07c5d399e6b52507be999 100644 --- a/src/lib/shell/pipe_exec/mod.rs +++ b/src/lib/shell/pipe_exec/mod.rs @@ -30,6 +30,7 @@ use crate::{ }; use smallvec::SmallVec; use std::{ + fmt, fs::{File, OpenOptions}, io::{self, Write}, iter, @@ -38,6 +39,111 @@ use std::{ process::{self, exit}, }; +#[derive(Debug, Error)] +pub enum InputError { + #[error(display = "ion: failed to redirect '{}' to stdin: {}", file, why)] + File { file: String, why: io::Error }, + #[error(display = "ion: failed to redirect herestring '{}' to stdin: {}", string, why)] + HereString { string: String, why: io::Error }, +} + +#[derive(Debug)] +pub struct OutputError { + redirect: RedirectFrom, + file: String, + why: io::Error, +} + +#[derive(Debug)] +pub enum RedirectError { + Input(InputError), + Output(OutputError), +} + +impl fmt::Display for OutputError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ion: failed to redirect {} to file '{}': {}", + match self.redirect { + RedirectFrom::Both => "both stdout and stderr", + RedirectFrom::Stdout => "stdout", + RedirectFrom::Stderr => "stderr", + _ => unreachable!(), + }, + self.file, + self.why, + ) + } +} + +impl std::error::Error for OutputError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.why) } +} + +impl fmt::Display for RedirectError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RedirectError::Output(why) => write!(f, "ion: failed to redirect {}", why), + RedirectError::Input(why) => write!(f, "ion: failed to redirect {}", why), + } + } +} + +impl std::error::Error for RedirectError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(match self { + RedirectError::Output(why) => why, + RedirectError::Input(why) => why, + }) + } +} + +impl From<OutputError> for RedirectError { + fn from(error: OutputError) -> Self { RedirectError::Output(error) } +} + +impl From<InputError> for RedirectError { + fn from(error: InputError) -> Self { RedirectError::Input(error) } +} + +/// Create an OS pipe and write the contents of a byte slice to one end +/// such that reading from this pipe will produce the byte slice. Return +/// A file descriptor representing the read end of the pipe. +pub unsafe fn stdin_of<T: AsRef<[u8]>>(input: T) -> Result<RawFd, io::Error> { + let (reader, writer) = sys::pipe2(sys::O_CLOEXEC)?; + let mut infile = File::from_raw_fd(writer); + // Write the contents; make sure to use write_all so that we block until + // the entire string is written + infile.write_all(input.as_ref())?; + infile.flush()?; + // `infile` currently owns the writer end RawFd. If we just return the reader + // end and let `infile` go out of scope, it will be closed, sending EOF to + // the reader! + Ok(reader) +} + +impl Input { + pub fn get_infile(&mut self) -> Result<File, InputError> { + match self { + Input::File(ref filename) => match File::open(filename.as_str()) { + Ok(file) => Ok(file), + Err(why) => Err(InputError::File { file: filename.to_string(), why }), + }, + Input::HereString(ref mut string) => { + if !string.ends_with('\n') { + string.push('\n'); + } + + match unsafe { stdin_of(&string) } { + Ok(stdio) => Ok(unsafe { File::from_raw_fd(stdio) }), + Err(why) => Err(InputError::HereString { string: string.to_string(), why }), + } + } + } + } +} + /// Determines if the supplied command implicitly defines to change the directory. /// /// This is detected by first checking if the argument starts with a '.' or an '/', or ends @@ -79,7 +185,7 @@ fn do_tee<'a>( job: &mut RefinedJob<'a>, stdout: &mut dyn FnMut(&mut RefinedJob<'a>, File), stderr: &mut dyn FnMut(&mut RefinedJob<'a>, File), -) -> Result<(), ()> { +) -> Result<(), OutputError> { // XXX: Possibly add an assertion here for correctness for output in outputs { match OpenOptions::new() @@ -88,27 +194,30 @@ fn do_tee<'a>( .append(output.append) .open(output.file.as_str()) { - Ok(f) => match output.from { + Ok(file) => match output.from { RedirectFrom::None => (), - RedirectFrom::Stdout => stdout(job, f), - RedirectFrom::Stderr => stderr(job, f), - RedirectFrom::Both => match f.try_clone() { + RedirectFrom::Stdout => stdout(job, file), + RedirectFrom::Stderr => stderr(job, file), + RedirectFrom::Both => match file.try_clone() { Ok(f_copy) => { - stdout(job, f); + stdout(job, file); stderr(job, f_copy); } - Err(e) => { - eprintln!( - "ion: failed to redirect both stdout and stderr to file '{:?}': {}", - f, e - ); - return Err(()); + Err(why) => { + return Err(OutputError { + redirect: output.from, + file: output.file.to_string(), + why, + }); } }, }, - Err(e) => { - eprintln!("ion: failed to redirect output into {}: {}", output.file, e); - return Err(()); + Err(why) => { + return Err(OutputError { + redirect: output.from, + file: output.file.to_string(), + why, + }); } } } @@ -120,7 +229,7 @@ fn do_tee<'a>( fn prepare<'a, 'b>( shell: &'a Shell<'b>, pipeline: Pipeline<'b>, -) -> Result<impl IntoIterator<Item = (RefinedJob<'b>, RedirectFrom)>, ()> { +) -> Result<impl IntoIterator<Item = (RefinedJob<'b>, RedirectFrom)>, RedirectError> { // Real logic begins here let mut new_commands = SmallVec::<[_; 16]>::with_capacity(2 * pipeline.items.len()); let mut prev_kind = RedirectFrom::None; @@ -349,7 +458,10 @@ impl<'b> Shell<'b> { fn pipe(&mut self, pipeline: Pipeline<'b>) -> Status { let mut commands = match prepare(self, pipeline) { Ok(c) => c.into_iter().peekable(), - Err(_) => return Status::COULD_NOT_EXEC, + Err(why) => { + eprintln!("{}", why); + return Status::COULD_NOT_EXEC; + } }; if let Some((mut parent, mut kind)) = commands.next() {