mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
Support multiple exit codes
..with more specific error enums.
This commit is contained in:
parent
2345a57940
commit
bb7130f960
23 changed files with 112 additions and 34 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2176,6 +2176,7 @@ dependencies = [
|
|||
"tempfile",
|
||||
"termcolor",
|
||||
"textwrap 0.13.4",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"toml_edit",
|
||||
]
|
||||
|
|
10
POLICY.md
10
POLICY.md
|
@ -35,12 +35,10 @@ Considered to be part of the API policy.
|
|||
|
||||
## CLI exit codes
|
||||
|
||||
- `0` No errors, normal exit.
|
||||
- `1` Generic error.
|
||||
- `2` Never used to avoid conflicts with Bash.
|
||||
- `3` Unable to execute with the given parameters.
|
||||
- `4` I/O error.
|
||||
- `5` Database error.
|
||||
- `0` - No errors, normal exit.
|
||||
- `1` - Generic error.
|
||||
- `2` - Never used to avoid conflicts with Bash.
|
||||
- `3` - Command-line Syntax Error.
|
||||
|
||||
# Security
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ version = "0.3.0"
|
|||
dirs-next = "^2.0.0"
|
||||
env_logger = "^0.8.3"
|
||||
anyhow = "1.0"
|
||||
thiserror = "1.0"
|
||||
log = "^0.4.14"
|
||||
nom = "^6.1.2"
|
||||
prettytable-rs = "^0.8.0"
|
||||
|
|
|
@ -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
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
59
cli/src/errors.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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.")?;
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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)? {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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"))?;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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()?;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -56,7 +56,8 @@ fn invalid_option() -> Result<(), Box<dyn std::error::Error>> {
|
|||
cmd.arg("--no-such-option");
|
||||
cmd.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains("command line not recognized"));
|
||||
.stderr(predicate::str::contains("command line not recognized"))
|
||||
.code(predicate::eq(3));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error, Eq, PartialEq, Clone)]
|
||||
#[non_exhaustive]
|
||||
/// Errors returned from taskchampion operations
|
||||
pub enum Error {
|
||||
#[error("Task Database Error: {}", _0)]
|
||||
DbError(String),
|
||||
#[error("Task Database Error: {0}")]
|
||||
Database(String),
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ mod taskdb;
|
|||
mod utils;
|
||||
mod workingset;
|
||||
|
||||
pub use errors::Error;
|
||||
pub use replica::Replica;
|
||||
pub use server::{Server, ServerConfig};
|
||||
pub use storage::StorageConfig;
|
||||
|
|
|
@ -113,7 +113,7 @@ impl Replica {
|
|||
// check that it already exists; this is a convenience check, as the task may already exist
|
||||
// when this Create operation is finally sync'd with operations from other replicas
|
||||
if self.taskdb.get_task(uuid)?.is_none() {
|
||||
return Err(Error::DbError(format!("Task {} does not exist", uuid)).into());
|
||||
return Err(Error::Database(format!("Task {} does not exist", uuid)).into());
|
||||
}
|
||||
self.taskdb.apply(Operation::Delete { uuid })?;
|
||||
trace!("task {} deleted", uuid);
|
||||
|
|
|
@ -49,12 +49,12 @@ impl TaskDb {
|
|||
Operation::Create { uuid } => {
|
||||
// insert if the task does not already exist
|
||||
if !txn.create_task(*uuid)? {
|
||||
return Err(Error::DbError(format!("Task {} already exists", uuid)).into());
|
||||
return Err(Error::Database(format!("Task {} already exists", uuid)).into());
|
||||
}
|
||||
}
|
||||
Operation::Delete { ref uuid } => {
|
||||
if !txn.delete_task(*uuid)? {
|
||||
return Err(Error::DbError(format!("Task {} does not exist", uuid)).into());
|
||||
return Err(Error::Database(format!("Task {} does not exist", uuid)).into());
|
||||
}
|
||||
}
|
||||
Operation::Update {
|
||||
|
@ -71,7 +71,7 @@ impl TaskDb {
|
|||
};
|
||||
txn.set_task(*uuid, task)?;
|
||||
} else {
|
||||
return Err(Error::DbError(format!("Task {} does not exist", uuid)).into());
|
||||
return Err(Error::Database(format!("Task {} does not exist", uuid)).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue