Support multiple exit codes

..with more specific error enums.
This commit is contained in:
Dustin J. Mitchell 2021-05-03 17:57:04 -04:00
parent 2345a57940
commit bb7130f960
23 changed files with 112 additions and 34 deletions

View file

@ -1,6 +1,5 @@
use super::args::*;
use super::{ArgList, Subcommand};
use anyhow::bail;
use nom::{combinator::*, sequence::*, Err, IResult};
/// A command is the overall command that the CLI should execute.
@ -29,13 +28,22 @@ impl Command {
}
/// Parse a command from the given list of strings.
pub fn from_argv(argv: &[&str]) -> anyhow::Result<Command> {
pub fn from_argv(argv: &[&str]) -> Result<Command, crate::Error> {
match Command::parse(argv) {
Ok((&[], cmd)) => Ok(cmd),
Ok((trailing, _)) => bail!("command line has trailing arguments: {:?}", trailing),
Ok((trailing, _)) => Err(crate::Error::for_arguments(format!(
"command line has trailing arguments: {:?}",
trailing
))),
Err(Err::Incomplete(_)) => unreachable!(),
Err(Err::Error(e)) => bail!("command line not recognized: {:?}", e),
Err(Err::Failure(e)) => bail!("command line not recognized: {:?}", e),
Err(Err::Error(e)) => Err(crate::Error::for_arguments(format!(
"command line not recognized: {:?}",
e
))),
Err(Err::Failure(e)) => Err(crate::Error::for_arguments(format!(
"command line not recognized: {:?}",
e
))),
}
}
}

View file

@ -1,8 +1,11 @@
use std::process::exit;
pub fn main() {
if let Err(err) = taskchampion_cli::main() {
eprintln!("{:?}", err);
exit(1);
match taskchampion_cli::main() {
Ok(_) => exit(0),
Err(e) => {
eprintln!("{:?}", e);
exit(e.exit_status());
}
}
}

59
cli/src/errors.rs Normal file
View file

@ -0,0 +1,59 @@
use taskchampion::Error as TcError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("Command-Line Syntax Error: {0}")]
Arguments(String),
#[error(transparent)]
TaskChampion(#[from] TcError),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl Error {
/// Construct a new command-line argument error
pub(crate) fn for_arguments<S: ToString>(msg: S) -> Self {
Error::Arguments(msg.to_string())
}
/// Determine the exit status for this error, as documented.
pub fn exit_status(&self) -> i32 {
match *self {
Error::Arguments(_) => 3,
_ => 1,
}
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
let err: anyhow::Error = err.into();
Error::Other(err)
}
}
#[cfg(test)]
mod test {
use super::*;
use anyhow::anyhow;
#[test]
fn test_exit_status() {
let mut err: Error;
err = anyhow!("uhoh").into();
assert_eq!(err.exit_status(), 1);
err = Error::Arguments("uhoh".to_string());
assert_eq!(err.exit_status(), 3);
err = std::io::Error::last_os_error().into();
assert_eq!(err.exit_status(), 1);
err = TcError::Database("uhoh".to_string()).into();
assert_eq!(err.exit_status(), 1);
}
}

View file

@ -6,7 +6,7 @@ pub(crate) fn execute<W: WriteColor>(
w: &mut W,
replica: &mut Replica,
modification: Modification,
) -> anyhow::Result<()> {
) -> Result<(), crate::Error> {
let description = match modification.description {
DescriptionMod::Set(ref s) => s.clone(),
_ => "(no description)".to_owned(),

View file

@ -6,7 +6,7 @@ pub(crate) fn execute<W: WriteColor>(
w: &mut W,
config_operation: ConfigOperation,
settings: &Settings,
) -> anyhow::Result<()> {
) -> Result<(), crate::Error> {
match config_operation {
ConfigOperation::Set(key, value) => {
let filename = settings.set(&key, &value)?;

View file

@ -1,7 +1,7 @@
use taskchampion::Replica;
use termcolor::WriteColor;
pub(crate) fn execute<W: WriteColor>(w: &mut W, replica: &mut Replica) -> anyhow::Result<()> {
pub(crate) fn execute<W: WriteColor>(w: &mut W, replica: &mut Replica) -> Result<(), crate::Error> {
log::debug!("rebuilding working set");
replica.rebuild_working_set(true)?;
writeln!(w, "garbage collected.")?;

View file

@ -5,7 +5,7 @@ pub(crate) fn execute<W: WriteColor>(
w: &mut W,
command_name: String,
summary: bool,
) -> anyhow::Result<()> {
) -> Result<(), crate::Error> {
let usage = Usage::new();
usage.write_help(w, command_name.as_ref(), summary)?;
Ok(())

View file

@ -10,7 +10,7 @@ pub(crate) fn execute<W: WriteColor>(
replica: &mut Replica,
filter: Filter,
debug: bool,
) -> anyhow::Result<()> {
) -> Result<(), crate::Error> {
let working_set = replica.working_set()?;
for task in filtered_tasks(replica, &filter)? {

View file

@ -8,7 +8,7 @@ pub(crate) fn execute<W: WriteColor>(
replica: &mut Replica,
filter: Filter,
modification: Modification,
) -> anyhow::Result<()> {
) -> Result<(), crate::Error> {
for task in filtered_tasks(replica, &filter)? {
let mut task = task.into_mut(replica);

View file

@ -10,7 +10,7 @@ pub(crate) fn execute<W: WriteColor>(
settings: &Settings,
report_name: String,
filter: Filter,
) -> anyhow::Result<()> {
) -> Result<(), crate::Error> {
display_report(w, replica, settings, report_name, filter)
}

View file

@ -5,7 +5,7 @@ pub(crate) fn execute<W: WriteColor>(
w: &mut W,
replica: &mut Replica,
server: &mut Box<dyn Server>,
) -> anyhow::Result<()> {
) -> Result<(), crate::Error> {
replica.sync(server)?;
writeln!(w, "sync complete.")?;
Ok(())

View file

@ -1,6 +1,6 @@
use termcolor::{ColorSpec, WriteColor};
pub(crate) fn execute<W: WriteColor>(w: &mut W) -> anyhow::Result<()> {
pub(crate) fn execute<W: WriteColor>(w: &mut W) -> Result<(), crate::Error> {
write!(w, "TaskChampion ")?;
w.set_color(ColorSpec::new().set_bold(true))?;
writeln!(w, "{}", env!("CARGO_PKG_VERSION"))?;

View file

@ -19,7 +19,7 @@ use report::display_report;
/// Invoke the given Command in the context of the given settings
#[allow(clippy::needless_return)]
pub(crate) fn invoke(command: Command, settings: Settings) -> anyhow::Result<()> {
pub(crate) fn invoke(command: Command, settings: Settings) -> Result<(), crate::Error> {
log::debug!("command: {:?}", command);
log::debug!("settings: {:?}", settings);

View file

@ -80,7 +80,7 @@ pub(super) fn display_report<W: WriteColor>(
settings: &Settings,
report_name: String,
filter: Filter,
) -> anyhow::Result<()> {
) -> Result<(), crate::Error> {
let mut t = Table::new();
let working_set = replica.working_set()?;

View file

@ -38,23 +38,26 @@ use std::string::FromUtf8Error;
mod macros;
mod argparse;
mod errors;
mod invocation;
mod settings;
mod table;
mod usage;
pub(crate) use errors::Error;
use settings::Settings;
/// The main entry point for the command-line interface. This builds an Invocation
/// from the particulars of the operating-system interface, and then executes it.
pub fn main() -> anyhow::Result<()> {
pub fn main() -> Result<(), Error> {
env_logger::init();
// parse the command line into a vector of &str, failing if
// there are invalid utf-8 sequences.
let argv: Vec<String> = std::env::args_os()
.map(|oss| String::from_utf8(oss.into_vec()))
.collect::<Result<_, FromUtf8Error>>()?;
.collect::<Result<_, FromUtf8Error>>()
.map_err(|_| Error::for_arguments("arguments must be valid utf-8"))?;
let argv: Vec<&str> = argv.iter().map(|s| s.as_ref()).collect();
// parse the command line