Refactor RunError::Backtick* to use OutputError (#186)

Add `output()` to get stdout of a command, return a OutputError if
it failes. Refactor backtick run errors to contain an OutputError.
This commit is contained in:
Casey Rodarmor 2017-04-23 14:21:21 -07:00 committed by GitHub
parent 84a55da1ce
commit af764f5eab
3 changed files with 96 additions and 77 deletions

View File

@ -7,7 +7,7 @@ use ::prelude::*;
use std::{convert, ffi}; use std::{convert, ffi};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use self::clap::{App, Arg, ArgGroup, AppSettings}; use self::clap::{App, Arg, ArgGroup, AppSettings};
use super::{Slurp, RunError, RunOptions, compile, DEFAULT_SHELL}; use super::{Slurp, RunOptions, compile, DEFAULT_SHELL};
macro_rules! warn { macro_rules! warn {
($($arg:tt)*) => {{ ($($arg:tt)*) => {{
@ -353,9 +353,7 @@ pub fn app() {
warn!("{}", run_error); warn!("{}", run_error);
} }
} }
match run_error {
RunError::Code{code, .. } | RunError::BacktickCode{code, ..} => process::exit(code), process::exit(run_error.code().unwrap_or(libc::EXIT_FAILURE));
_ => process::exit(libc::EXIT_FAILURE),
}
} }
} }

View File

@ -101,6 +101,48 @@ fn contains<T: PartialOrd>(range: &Range<T>, i: T) -> bool {
i >= range.start && i < range.end i >= range.start && i < range.end
} }
#[derive(Debug)]
enum OutputError {
/// Non-zero exit code
Code(i32),
/// IO error upon execution
Io(io::Error),
/// Terminated by signal
Signal(i32),
/// Unknown failure
Unknown,
/// Stdtout not UTF-8
Utf8(std::str::Utf8Error),
}
/// Run a command and return the data it wrote to stdout as a string
fn output(mut command: process::Command) -> Result<String, OutputError> {
match command.output() {
Ok(output) => {
if let Some(code) = output.status.code() {
if code != 0 {
return Err(OutputError::Code(code));
}
} else {
return Err(output_error_from_signal(output.status));
}
match std::str::from_utf8(&output.stdout) {
Err(error) => Err(OutputError::Utf8(error)),
Ok(utf8) => {
Ok(if utf8.ends_with('\n') {
&utf8[0..utf8.len()-1]
} else if utf8.ends_with("\r\n") {
&utf8[0..utf8.len()-2]
} else {
utf8
}.to_string())
}
}
}
Err(io_error) => Err(OutputError::Io(io_error)),
}
}
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
struct Recipe<'a> { struct Recipe<'a> {
dependencies: Vec<&'a str>, dependencies: Vec<&'a str>,
@ -215,15 +257,12 @@ fn error_from_signal(
} }
} }
/// Return a RunError::BacktickSignal if the process was terminated by signal, /// Return an OutputError::Signal if the process was terminated by signal,
/// otherwise return a RunError::BacktickUnknownFailure /// otherwise return an OutputError::UnknownFailure
fn backtick_error_from_signal<'a>( fn output_error_from_signal(exit_status: process::ExitStatus) -> OutputError {
token: &Token<'a>,
exit_status: process::ExitStatus
) -> RunError<'a> {
match Platform::signal_from_exit_status(exit_status) { match Platform::signal_from_exit_status(exit_status) {
Some(signal) => RunError::BacktickSignal{token: token.clone(), signal: signal}, Some(signal) => OutputError::Signal(signal),
None => RunError::BacktickUnknownFailure{token: token.clone()}, None => OutputError::Unknown,
} }
} }
@ -264,33 +303,7 @@ fn run_backtick<'a>(
process::Stdio::inherit() process::Stdio::inherit()
}); });
match cmd.output() { output(cmd).map_err(|output_error| RunError::Backtick{token: token.clone(), output_error: output_error})
Ok(output) => {
if let Some(code) = output.status.code() {
if code != 0 {
return Err(RunError::BacktickCode {
token: token.clone(),
code: code,
});
}
} else {
return Err(backtick_error_from_signal(token, output.status));
}
match std::str::from_utf8(&output.stdout) {
Err(error) => Err(RunError::BacktickUtf8Error{token: token.clone(), utf8_error: error}),
Ok(utf8) => {
Ok(if utf8.ends_with('\n') {
&utf8[0..utf8.len()-1]
} else if utf8.ends_with("\r\n") {
&utf8[0..utf8.len()-2]
} else {
utf8
}.to_string())
}
}
}
Err(error) => Err(RunError::BacktickIoError{token: token.clone(), io_error: error}),
}
} }
impl<'a> Recipe<'a> { impl<'a> Recipe<'a> {
@ -1334,11 +1347,7 @@ impl<'a> Display for Justfile<'a> {
#[derive(Debug)] #[derive(Debug)]
enum RunError<'a> { enum RunError<'a> {
ArgumentCountMismatch{recipe: &'a str, found: usize, min: usize, max: usize}, ArgumentCountMismatch{recipe: &'a str, found: usize, min: usize, max: usize},
BacktickCode{token: Token<'a>, code: i32}, Backtick{token: Token<'a>, output_error: OutputError},
BacktickIoError{token: Token<'a>, io_error: io::Error},
BacktickSignal{token: Token<'a>, signal: i32},
BacktickUnknownFailure{token: Token<'a>},
BacktickUtf8Error{token: Token<'a>, utf8_error: std::str::Utf8Error},
Code{recipe: &'a str, line_number: Option<usize>, code: i32}, Code{recipe: &'a str, line_number: Option<usize>, code: i32},
InternalError{message: String}, InternalError{message: String},
IoError{recipe: &'a str, io_error: io::Error}, IoError{recipe: &'a str, io_error: io::Error},
@ -1350,6 +1359,16 @@ enum RunError<'a> {
UnknownRecipes{recipes: Vec<&'a str>, suggestion: Option<&'a str>}, UnknownRecipes{recipes: Vec<&'a str>, suggestion: Option<&'a str>},
} }
impl<'a> RunError<'a> {
fn code(&self) -> Option<i32> {
use RunError::*;
match *self {
Code{code, ..} | Backtick{output_error: OutputError::Code(code), ..} => Some(code),
_ => None,
}
}
}
impl<'a> Display for RunError<'a> { impl<'a> Display for RunError<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use RunError::*; use RunError::*;
@ -1431,19 +1450,20 @@ impl<'a> Display for RunError<'a> {
write!(f, "Recipe `{}` could not be run because of an IO error while trying \ write!(f, "Recipe `{}` could not be run because of an IO error while trying \
to create a temporary directory or write a file to that directory`:\n{}", to create a temporary directory or write a file to that directory`:\n{}",
recipe, io_error)?, recipe, io_error)?,
BacktickCode{code, ref token} => { Backtick{ref token, ref output_error} => match *output_error {
OutputError::Code(code) => {
write!(f, "Backtick failed with exit code {}\n", code)?; write!(f, "Backtick failed with exit code {}\n", code)?;
error_token = Some(token); error_token = Some(token);
} }
BacktickSignal{ref token, signal} => { OutputError::Signal(signal) => {
write!(f, "Backtick was terminated by signal {}", signal)?; write!(f, "Backtick was terminated by signal {}", signal)?;
error_token = Some(token); error_token = Some(token);
} }
BacktickUnknownFailure{ref token} => { OutputError::Unknown => {
write!(f, "Backtick failed for an unknown reason")?; write!(f, "Backtick failed for an unknown reason")?;
error_token = Some(token); error_token = Some(token);
} }
BacktickIoError{ref token, ref io_error} => { OutputError::Io(ref io_error) => {
match io_error.kind() { match io_error.kind() {
io::ErrorKind::NotFound => write!( io::ErrorKind::NotFound => write!(
f, "Backtick could not be run because just could not find `sh` the command:\n{}", f, "Backtick could not be run because just could not find `sh` the command:\n{}",
@ -1455,10 +1475,11 @@ impl<'a> Display for RunError<'a> {
}?; }?;
error_token = Some(token); error_token = Some(token);
} }
BacktickUtf8Error{ref token, ref utf8_error} => { OutputError::Utf8(ref utf8_error) => {
write!(f, "Backtick succeeded but stdout was not utf8: {}", utf8_error)?; write!(f, "Backtick succeeded but stdout was not utf8: {}", utf8_error)?;
error_token = Some(token); error_token = Some(token);
} }
},
InternalError{ref message} => { InternalError{ref message} => {
write!(f, "Internal error, this may indicate a bug in just: {} \ write!(f, "Internal error, this may indicate a bug in just: {} \
consider filing an issue: https://github.com/casey/just/issues/new", consider filing an issue: https://github.com/casey/just/issues/new",

View File

@ -3,8 +3,8 @@ extern crate brev;
use super::{ use super::{
And, CompileError, ErrorKind, Justfile, Or, And, CompileError, ErrorKind, Justfile, Or,
RunError, RunOptions, Token, compile, contains, OutputError, RunError, RunOptions, Token,
tokenize compile, contains, tokenize
}; };
use super::TokenKind::*; use super::TokenKind::*;
@ -1016,7 +1016,7 @@ fn missing_all_defaults() {
fn backtick_code() { fn backtick_code() {
match parse_success("a:\n echo {{`f() { return 100; }; f`}}") match parse_success("a:\n echo {{`f() { return 100; }; f`}}")
.run(&["a"], &Default::default()).unwrap_err() { .run(&["a"], &Default::default()).unwrap_err() {
RunError::BacktickCode{code, token} => { RunError::Backtick{token, output_error: OutputError::Code(code)} => {
assert_eq!(code, 100); assert_eq!(code, 100);
assert_eq!(token.lexeme, "`f() { return 100; }; f`"); assert_eq!(token.lexeme, "`f() { return 100; }; f`");
}, },
@ -1054,7 +1054,7 @@ recipe:
}; };
match parse_success(text).run(&["recipe"], &options).unwrap_err() { match parse_success(text).run(&["recipe"], &options).unwrap_err() {
RunError::BacktickCode{code: _, token} => { RunError::Backtick{token, output_error: OutputError::Code(_)} => {
assert_eq!(token.lexeme, "`echo $exported_variable`"); assert_eq!(token.lexeme, "`echo $exported_variable`");
}, },
other => panic!("expected a backtick code errror, but got: {}", other), other => panic!("expected a backtick code errror, but got: {}", other),