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::collections::BTreeMap;
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 {
($($arg:tt)*) => {{
@ -353,9 +353,7 @@ pub fn app() {
warn!("{}", run_error);
}
}
match run_error {
RunError::Code{code, .. } | RunError::BacktickCode{code, ..} => process::exit(code),
_ => process::exit(libc::EXIT_FAILURE),
}
process::exit(run_error.code().unwrap_or(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
}
#[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)]
struct Recipe<'a> {
dependencies: Vec<&'a str>,
@ -215,15 +257,12 @@ fn error_from_signal(
}
}
/// Return a RunError::BacktickSignal if the process was terminated by signal,
/// otherwise return a RunError::BacktickUnknownFailure
fn backtick_error_from_signal<'a>(
token: &Token<'a>,
exit_status: process::ExitStatus
) -> RunError<'a> {
/// Return an OutputError::Signal if the process was terminated by signal,
/// otherwise return an OutputError::UnknownFailure
fn output_error_from_signal(exit_status: process::ExitStatus) -> OutputError {
match Platform::signal_from_exit_status(exit_status) {
Some(signal) => RunError::BacktickSignal{token: token.clone(), signal: signal},
None => RunError::BacktickUnknownFailure{token: token.clone()},
Some(signal) => OutputError::Signal(signal),
None => OutputError::Unknown,
}
}
@ -264,33 +303,7 @@ fn run_backtick<'a>(
process::Stdio::inherit()
});
match cmd.output() {
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}),
}
output(cmd).map_err(|output_error| RunError::Backtick{token: token.clone(), output_error: output_error})
}
impl<'a> Recipe<'a> {
@ -1334,11 +1347,7 @@ impl<'a> Display for Justfile<'a> {
#[derive(Debug)]
enum RunError<'a> {
ArgumentCountMismatch{recipe: &'a str, found: usize, min: usize, max: usize},
BacktickCode{token: Token<'a>, code: i32},
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},
Backtick{token: Token<'a>, output_error: OutputError},
Code{recipe: &'a str, line_number: Option<usize>, code: i32},
InternalError{message: String},
IoError{recipe: &'a str, io_error: io::Error},
@ -1350,6 +1359,16 @@ enum RunError<'a> {
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> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use RunError::*;
@ -1431,34 +1450,36 @@ impl<'a> Display for RunError<'a> {
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{}",
recipe, io_error)?,
BacktickCode{code, ref token} => {
write!(f, "Backtick failed with exit code {}\n", code)?;
error_token = Some(token);
}
BacktickSignal{ref token, signal} => {
write!(f, "Backtick was terminated by signal {}", signal)?;
error_token = Some(token);
}
BacktickUnknownFailure{ref token} => {
write!(f, "Backtick failed for an unknown reason")?;
error_token = Some(token);
}
BacktickIoError{ref token, ref io_error} => {
match io_error.kind() {
io::ErrorKind::NotFound => write!(
f, "Backtick could not be run because just could not find `sh` the command:\n{}",
io_error),
io::ErrorKind::PermissionDenied => write!(
f, "Backtick could not be run because just could not run `sh`:\n{}", io_error),
_ => write!(f, "Backtick could not be run because of an IO \
error while launching `sh`:\n{}", io_error),
}?;
error_token = Some(token);
}
BacktickUtf8Error{ref token, ref utf8_error} => {
write!(f, "Backtick succeeded but stdout was not utf8: {}", utf8_error)?;
error_token = Some(token);
}
Backtick{ref token, ref output_error} => match *output_error {
OutputError::Code(code) => {
write!(f, "Backtick failed with exit code {}\n", code)?;
error_token = Some(token);
}
OutputError::Signal(signal) => {
write!(f, "Backtick was terminated by signal {}", signal)?;
error_token = Some(token);
}
OutputError::Unknown => {
write!(f, "Backtick failed for an unknown reason")?;
error_token = Some(token);
}
OutputError::Io(ref io_error) => {
match io_error.kind() {
io::ErrorKind::NotFound => write!(
f, "Backtick could not be run because just could not find `sh` the command:\n{}",
io_error),
io::ErrorKind::PermissionDenied => write!(
f, "Backtick could not be run because just could not run `sh`:\n{}", io_error),
_ => write!(f, "Backtick could not be run because of an IO \
error while launching `sh`:\n{}", io_error),
}?;
error_token = Some(token);
}
OutputError::Utf8(ref utf8_error) => {
write!(f, "Backtick succeeded but stdout was not utf8: {}", utf8_error)?;
error_token = Some(token);
}
},
InternalError{ref message} => {
write!(f, "Internal error, this may indicate a bug in just: {} \
consider filing an issue: https://github.com/casey/just/issues/new",

View File

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