mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-07-07 20:06:36 +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",
|
"tempfile",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
"textwrap 0.13.4",
|
"textwrap 0.13.4",
|
||||||
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
"toml_edit",
|
"toml_edit",
|
||||||
]
|
]
|
||||||
|
|
10
POLICY.md
10
POLICY.md
|
@ -35,12 +35,10 @@ Considered to be part of the API policy.
|
||||||
|
|
||||||
## CLI exit codes
|
## CLI exit codes
|
||||||
|
|
||||||
- `0` No errors, normal exit.
|
- `0` - No errors, normal exit.
|
||||||
- `1` Generic error.
|
- `1` - Generic error.
|
||||||
- `2` Never used to avoid conflicts with Bash.
|
- `2` - Never used to avoid conflicts with Bash.
|
||||||
- `3` Unable to execute with the given parameters.
|
- `3` - Command-line Syntax Error.
|
||||||
- `4` I/O error.
|
|
||||||
- `5` Database error.
|
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ version = "0.3.0"
|
||||||
dirs-next = "^2.0.0"
|
dirs-next = "^2.0.0"
|
||||||
env_logger = "^0.8.3"
|
env_logger = "^0.8.3"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
thiserror = "1.0"
|
||||||
log = "^0.4.14"
|
log = "^0.4.14"
|
||||||
nom = "^6.1.2"
|
nom = "^6.1.2"
|
||||||
prettytable-rs = "^0.8.0"
|
prettytable-rs = "^0.8.0"
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use super::args::*;
|
use super::args::*;
|
||||||
use super::{ArgList, Subcommand};
|
use super::{ArgList, Subcommand};
|
||||||
use anyhow::bail;
|
|
||||||
use nom::{combinator::*, sequence::*, Err, IResult};
|
use nom::{combinator::*, sequence::*, Err, IResult};
|
||||||
|
|
||||||
/// A command is the overall command that the CLI should execute.
|
/// 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.
|
/// 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) {
|
match Command::parse(argv) {
|
||||||
Ok((&[], cmd)) => Ok(cmd),
|
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::Incomplete(_)) => unreachable!(),
|
||||||
Err(Err::Error(e)) => bail!("command line not recognized: {:?}", e),
|
Err(Err::Error(e)) => Err(crate::Error::for_arguments(format!(
|
||||||
Err(Err::Failure(e)) => bail!("command line not recognized: {:?}", e),
|
"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;
|
use std::process::exit;
|
||||||
|
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
if let Err(err) = taskchampion_cli::main() {
|
match taskchampion_cli::main() {
|
||||||
eprintln!("{:?}", err);
|
Ok(_) => exit(0),
|
||||||
exit(1);
|
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,
|
w: &mut W,
|
||||||
replica: &mut Replica,
|
replica: &mut Replica,
|
||||||
modification: Modification,
|
modification: Modification,
|
||||||
) -> anyhow::Result<()> {
|
) -> Result<(), crate::Error> {
|
||||||
let description = match modification.description {
|
let description = match modification.description {
|
||||||
DescriptionMod::Set(ref s) => s.clone(),
|
DescriptionMod::Set(ref s) => s.clone(),
|
||||||
_ => "(no description)".to_owned(),
|
_ => "(no description)".to_owned(),
|
||||||
|
|
|
@ -6,7 +6,7 @@ pub(crate) fn execute<W: WriteColor>(
|
||||||
w: &mut W,
|
w: &mut W,
|
||||||
config_operation: ConfigOperation,
|
config_operation: ConfigOperation,
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
) -> anyhow::Result<()> {
|
) -> Result<(), crate::Error> {
|
||||||
match config_operation {
|
match config_operation {
|
||||||
ConfigOperation::Set(key, value) => {
|
ConfigOperation::Set(key, value) => {
|
||||||
let filename = settings.set(&key, &value)?;
|
let filename = settings.set(&key, &value)?;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use taskchampion::Replica;
|
use taskchampion::Replica;
|
||||||
use termcolor::WriteColor;
|
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");
|
log::debug!("rebuilding working set");
|
||||||
replica.rebuild_working_set(true)?;
|
replica.rebuild_working_set(true)?;
|
||||||
writeln!(w, "garbage collected.")?;
|
writeln!(w, "garbage collected.")?;
|
||||||
|
|
|
@ -5,7 +5,7 @@ pub(crate) fn execute<W: WriteColor>(
|
||||||
w: &mut W,
|
w: &mut W,
|
||||||
command_name: String,
|
command_name: String,
|
||||||
summary: bool,
|
summary: bool,
|
||||||
) -> anyhow::Result<()> {
|
) -> Result<(), crate::Error> {
|
||||||
let usage = Usage::new();
|
let usage = Usage::new();
|
||||||
usage.write_help(w, command_name.as_ref(), summary)?;
|
usage.write_help(w, command_name.as_ref(), summary)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -10,7 +10,7 @@ pub(crate) fn execute<W: WriteColor>(
|
||||||
replica: &mut Replica,
|
replica: &mut Replica,
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
) -> anyhow::Result<()> {
|
) -> Result<(), crate::Error> {
|
||||||
let working_set = replica.working_set()?;
|
let working_set = replica.working_set()?;
|
||||||
|
|
||||||
for task in filtered_tasks(replica, &filter)? {
|
for task in filtered_tasks(replica, &filter)? {
|
||||||
|
|
|
@ -8,7 +8,7 @@ pub(crate) fn execute<W: WriteColor>(
|
||||||
replica: &mut Replica,
|
replica: &mut Replica,
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
modification: Modification,
|
modification: Modification,
|
||||||
) -> anyhow::Result<()> {
|
) -> Result<(), crate::Error> {
|
||||||
for task in filtered_tasks(replica, &filter)? {
|
for task in filtered_tasks(replica, &filter)? {
|
||||||
let mut task = task.into_mut(replica);
|
let mut task = task.into_mut(replica);
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ pub(crate) fn execute<W: WriteColor>(
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
report_name: String,
|
report_name: String,
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
) -> anyhow::Result<()> {
|
) -> Result<(), crate::Error> {
|
||||||
display_report(w, replica, settings, report_name, filter)
|
display_report(w, replica, settings, report_name, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ pub(crate) fn execute<W: WriteColor>(
|
||||||
w: &mut W,
|
w: &mut W,
|
||||||
replica: &mut Replica,
|
replica: &mut Replica,
|
||||||
server: &mut Box<dyn Server>,
|
server: &mut Box<dyn Server>,
|
||||||
) -> anyhow::Result<()> {
|
) -> Result<(), crate::Error> {
|
||||||
replica.sync(server)?;
|
replica.sync(server)?;
|
||||||
writeln!(w, "sync complete.")?;
|
writeln!(w, "sync complete.")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use termcolor::{ColorSpec, WriteColor};
|
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 ")?;
|
write!(w, "TaskChampion ")?;
|
||||||
w.set_color(ColorSpec::new().set_bold(true))?;
|
w.set_color(ColorSpec::new().set_bold(true))?;
|
||||||
writeln!(w, "{}", env!("CARGO_PKG_VERSION"))?;
|
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
|
/// Invoke the given Command in the context of the given settings
|
||||||
#[allow(clippy::needless_return)]
|
#[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!("command: {:?}", command);
|
||||||
log::debug!("settings: {:?}", settings);
|
log::debug!("settings: {:?}", settings);
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@ pub(super) fn display_report<W: WriteColor>(
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
report_name: String,
|
report_name: String,
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
) -> anyhow::Result<()> {
|
) -> Result<(), crate::Error> {
|
||||||
let mut t = Table::new();
|
let mut t = Table::new();
|
||||||
let working_set = replica.working_set()?;
|
let working_set = replica.working_set()?;
|
||||||
|
|
||||||
|
|
|
@ -38,23 +38,26 @@ use std::string::FromUtf8Error;
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
mod argparse;
|
mod argparse;
|
||||||
|
mod errors;
|
||||||
mod invocation;
|
mod invocation;
|
||||||
mod settings;
|
mod settings;
|
||||||
mod table;
|
mod table;
|
||||||
mod usage;
|
mod usage;
|
||||||
|
|
||||||
|
pub(crate) use errors::Error;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
|
||||||
/// The main entry point for the command-line interface. This builds an Invocation
|
/// 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.
|
/// 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();
|
env_logger::init();
|
||||||
|
|
||||||
// parse the command line into a vector of &str, failing if
|
// parse the command line into a vector of &str, failing if
|
||||||
// there are invalid utf-8 sequences.
|
// there are invalid utf-8 sequences.
|
||||||
let argv: Vec<String> = std::env::args_os()
|
let argv: Vec<String> = std::env::args_os()
|
||||||
.map(|oss| String::from_utf8(oss.into_vec()))
|
.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();
|
let argv: Vec<&str> = argv.iter().map(|s| s.as_ref()).collect();
|
||||||
|
|
||||||
// parse the command line
|
// parse the command line
|
||||||
|
|
|
@ -56,7 +56,8 @@ fn invalid_option() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
cmd.arg("--no-such-option");
|
cmd.arg("--no-such-option");
|
||||||
cmd.assert()
|
cmd.assert()
|
||||||
.failure()
|
.failure()
|
||||||
.stderr(predicate::str::contains("command line not recognized"));
|
.stderr(predicate::str::contains("command line not recognized"))
|
||||||
|
.code(predicate::eq(3));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Error, Eq, PartialEq, Clone)]
|
#[derive(Debug, Error, Eq, PartialEq, Clone)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
/// Errors returned from taskchampion operations
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Task Database Error: {}", _0)]
|
#[error("Task Database Error: {0}")]
|
||||||
DbError(String),
|
Database(String),
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ mod taskdb;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod workingset;
|
mod workingset;
|
||||||
|
|
||||||
|
pub use errors::Error;
|
||||||
pub use replica::Replica;
|
pub use replica::Replica;
|
||||||
pub use server::{Server, ServerConfig};
|
pub use server::{Server, ServerConfig};
|
||||||
pub use storage::StorageConfig;
|
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
|
// 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
|
// when this Create operation is finally sync'd with operations from other replicas
|
||||||
if self.taskdb.get_task(uuid)?.is_none() {
|
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 })?;
|
self.taskdb.apply(Operation::Delete { uuid })?;
|
||||||
trace!("task {} deleted", uuid);
|
trace!("task {} deleted", uuid);
|
||||||
|
|
|
@ -49,12 +49,12 @@ impl TaskDb {
|
||||||
Operation::Create { uuid } => {
|
Operation::Create { uuid } => {
|
||||||
// insert if the task does not already exist
|
// insert if the task does not already exist
|
||||||
if !txn.create_task(*uuid)? {
|
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 } => {
|
Operation::Delete { ref uuid } => {
|
||||||
if !txn.delete_task(*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 {
|
Operation::Update {
|
||||||
|
@ -71,7 +71,7 @@ impl TaskDb {
|
||||||
};
|
};
|
||||||
txn.set_task(*uuid, task)?;
|
txn.set_task(*uuid, task)?;
|
||||||
} else {
|
} 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