fix(cli): Define an error code policy

The main goal is to make spelling errors differentiated from other
errors.

Fixes #170
This commit is contained in:
Ed Page 2020-11-14 19:53:51 -06:00
parent 8a35a12096
commit 4ddbdcf5dd
4 changed files with 97 additions and 14 deletions

10
Cargo.lock generated
View file

@ -973,6 +973,15 @@ dependencies = [
"unicode-xid 0.2.1", "unicode-xid 0.2.1",
] ]
[[package]]
name = "sysexit"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26991bb0e6ef939ac9a8d6b57979efa59921928a3aa0334d818fe6e458aa6a9"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "tap" name = "tap"
version = "1.0.0" version = "1.0.0"
@ -1092,6 +1101,7 @@ dependencies = [
"predicates", "predicates",
"serde", "serde",
"structopt", "structopt",
"sysexit",
"toml", "toml",
"typos", "typos",
"typos-dict", "typos-dict",

View file

@ -48,6 +48,7 @@ env_logger = "0.8"
bstr = "0.2" bstr = "0.2"
ahash = "0.5.8" ahash = "0.5.8"
difflib = "0.4" difflib = "0.4"
sysexit = "0.2"
[dev-dependencies] [dev-dependencies]
assert_fs = "1.0" assert_fs = "1.0"

55
src/exit.rs Normal file
View file

@ -0,0 +1,55 @@
pub struct ExitCode {
pub code: sysexit::Code,
pub error: Option<anyhow::Error>,
}
impl ExitCode {
pub fn code(code: sysexit::Code) -> Self {
Self { code, error: None }
}
pub fn error(mut self, error: anyhow::Error) -> Self {
self.error = Some(error);
self
}
}
impl From<sysexit::Code> for ExitCode {
fn from(code: sysexit::Code) -> Self {
Self::code(code)
}
}
pub trait ChainCodeExt {
fn error(self) -> ExitCode;
fn chain(self, error: anyhow::Error) -> ExitCode;
}
impl ChainCodeExt for sysexit::Code {
fn error(self) -> ExitCode {
ExitCode::code(self)
}
fn chain(self, error: anyhow::Error) -> ExitCode {
ExitCode::code(self).error(error)
}
}
pub trait ExitCodeResultErrorExt<T> {
fn code(self, code: sysexit::Code) -> Result<T, ExitCode>;
}
impl<T, E: std::error::Error + Send + Sync + 'static> ExitCodeResultErrorExt<T> for Result<T, E> {
fn code(self, code: sysexit::Code) -> Result<T, ExitCode> {
self.map_err(|e| ExitCode::code(code).error(e.into()))
}
}
pub trait ExitCodeResultAnyhowExt<T> {
fn code(self, code: sysexit::Code) -> Result<T, ExitCode>;
}
impl<T> ExitCodeResultAnyhowExt<T> for Result<T, anyhow::Error> {
fn code(self, code: sysexit::Code) -> Result<T, ExitCode> {
self.map_err(|e| ExitCode::code(code).error(e))
}
}

View file

@ -11,26 +11,43 @@ mod checks;
mod config; mod config;
mod dict; mod dict;
mod diff; mod diff;
mod exit;
mod replace; mod replace;
use exit::ChainCodeExt;
use exit::ExitCodeResultAnyhowExt;
use exit::ExitCodeResultErrorExt;
fn main() { fn main() {
let code = match run() { let code = match run() {
Ok(code) => code, Ok(()) => sysexit::Code::Success,
Err(err) => { Err(err) => {
eprintln!("{}", err); if let Some(error) = err.error {
1 eprintln!("{}", error);
}
err.code
} }
}; };
std::process::exit(code); std::process::exit(code as i32);
} }
fn run() -> Result<i32, anyhow::Error> { fn run() -> Result<(), exit::ExitCode> {
let args = args::Args::from_args(); // clap's `get_matches` uses Failure rather than Usage, so bypass it for `get_matches_safe`.
let args = match args::Args::from_args_safe() {
Ok(args) => args,
Err(e) if e.use_stderr() => {
return Err(sysexit::Code::Usage.chain(e.into()));
}
Err(e) => {
println!("{}", e);
return Ok(());
}
};
init_logging(args.verbose.log_level()); init_logging(args.verbose.log_level());
let config = if let Some(path) = args.custom_config.as_ref() { let config = if let Some(path) = args.custom_config.as_ref() {
config::Config::from_file(path)? config::Config::from_file(path).code(sysexit::Code::Config)?
} else { } else {
config::Config::default() config::Config::default()
}; };
@ -38,7 +55,7 @@ fn run() -> Result<i32, anyhow::Error> {
let mut typos_found = false; let mut typos_found = false;
let mut errors_found = false; let mut errors_found = false;
for path in args.path.iter() { for path in args.path.iter() {
let path = path.canonicalize()?; let path = path.canonicalize().code(sysexit::Code::Usage)?;
let cwd = if path.is_file() { let cwd = if path.is_file() {
path.parent().unwrap() path.parent().unwrap()
} else { } else {
@ -47,7 +64,7 @@ fn run() -> Result<i32, anyhow::Error> {
let mut config = config.clone(); let mut config = config.clone();
if !args.isolated { if !args.isolated {
let derived = config::Config::derive(cwd)?; let derived = config::Config::derive(cwd).code(sysexit::Code::Config)?;
config.update(&derived); config.update(&derived);
} }
config.update(&args.config); config.update(&args.config);
@ -134,18 +151,18 @@ fn run() -> Result<i32, anyhow::Error> {
} }
if args.diff { if args.diff {
diff_reporter.show()?; diff_reporter.show().code(sysexit::Code::Unknown)?;
} else if args.write_changes { } else if args.write_changes {
replace_reporter.write()?; replace_reporter.write().code(sysexit::Code::Unknown)?;
} }
} }
if errors_found { if errors_found {
Ok(2) Err(sysexit::Code::Failure.error())
} else if typos_found { } else if typos_found {
Ok(1) Err(sysexit::Code::DataErr.error())
} else { } else {
Ok(0) Ok(())
} }
} }