diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d3bfad8..0b472f4 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -80,7 +80,7 @@ jobs: # https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability strategy: matrix: - msrv: [1.70.0] + msrv: [1.74.0] name: ubuntu / ${{ matrix.msrv }} steps: - uses: actions/checkout@v4 diff --git a/Cargo.toml b/Cargo.toml index a27e9aa..840a974 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,11 +3,11 @@ resolver = "2" members = ["cling", "cling-derive"] [workspace.package] -version = "0.1.1" +version = "0.1.2" edition = "2021" authors = ["Ahmed Farghal "] license = "Apache-2.0 OR MIT" -rust-version = "1.70.0" # MSRV +rust-version = "1.74.0" # MSRV keywords = ["click", "cli", "framework"] repository = "https://github.com/AhmedSoliman/cling" diff --git a/README.md b/README.md index e8cb994..15184aa 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ For more details, see: - [docs.rs](https://docs.rs/cling/latest/cling/) - [examples](examples/) -*Compiler support: [requires `rustc` 1.70+][msrv]* +*Compiler support: [requires `rustc` 1.74+][msrv]* [msrv]: #supported-rust-versions @@ -56,6 +56,7 @@ edition = "2021" [dependencies] clap = { version = "4.4.1", features = ["derive", "env"] } +cling = { version = "0.1" } tokio = { version = "1.13.0", features = ["full"] } ``` @@ -341,7 +342,7 @@ Beep beep! # Supported Rust Versions -Cling's minimum supported rust version is `1.70.0`. +Cling's minimum supported rust version is `1.74.0`. # License diff --git a/cling-derive/Cargo.toml b/cling-derive/Cargo.toml index 39851dc..7b0fa25 100644 --- a/cling-derive/Cargo.toml +++ b/cling-derive/Cargo.toml @@ -6,7 +6,7 @@ version.workspace = true authors.workspace = true license.workspace = true repository.workspace = true -rust-version = "1.70.0" +rust-version.workspace = true categories = [ "command-line-interface", "development-tools::procedural-macro-helpers", diff --git a/cling/Cargo.toml b/cling/Cargo.toml index 0bc5d44..c97c5d3 100644 --- a/cling/Cargo.toml +++ b/cling/Cargo.toml @@ -6,24 +6,25 @@ version.workspace = true authors.workspace = true license.workspace = true repository.workspace = true -rust-version = "1.70.0" +rust-version.workspace = true categories = ["command-line-interface"] include = ["**/*.rs", "../README.md", "../LICENSE-*", "../examples"] readme = "../README.md" [dependencies] cling-derive = { path = "../cling-derive", version = "0.1", optional = true } -clap = { version = "4.3.21", default-features = false, features = [ +clap = { version = "4", default-features = false, features = [ "std", "derive", ] } anyhow = { version = "1.0" } -async-trait = { version = "0.1.70" } +async-trait = { version = "0.1.77" } indoc = { version = "2.0" } +itertools = { version = "0.12.1" } rustversion = "1.0.14" -shlex = { version = "1.1.0", optional = true } +shlex = { version = "1.3.0", optional = true } static_assertions = { workspace = true } -termcolor = { version = "1.2" } +termcolor = { version = "1.4" } tracing = { version = "0.1.37", features = ["log"] } @@ -40,8 +41,8 @@ colored = { version = "2.0" } # Use clap with default features in tests clap = { version = "4.3.21", default-features = true, features = ["derive"] } # For testing collecting external types -clap-verbosity-flag = { version = "2.0.1" } -env_logger = { version = "0.11.0" } +clap-verbosity-flag = { version = "2.2.0" } +env_logger = { version = "0.11.3" } log = { version = "0.4.20" } [build-dependencies] @@ -82,6 +83,10 @@ required-features = ["shlex"] name = "many-handlers" path = "../examples/many_handlers.rs" +[[example]] +name = "errors" +path = "../examples/errors.rs" + [package.metadata.docs.rs] all-features = true default-target = "x86_64-unknown-linux-gnu" diff --git a/cling/src/error.rs b/cling/src/error.rs index bf4b735..649061a 100644 --- a/cling/src/error.rs +++ b/cling/src/error.rs @@ -2,6 +2,7 @@ use std::fmt::{self, Display, Formatter}; use std::io::Write; use clap::CommandFactory; +use itertools::{Itertools, Position}; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; use crate::prelude::ClingFinished; @@ -101,10 +102,10 @@ impl Display for CliError { write!(f, "Error: {}", e) } | CliError::Other(e) => { - write!(f, "Error: {}", e) + write!(f, "Error: {:#}", e) } | CliError::OtherWithCode(e, _) => { - write!(f, "Error: {}", e) + write!(f, "Error: {:#}", e) } | CliError::InputString => { write!(f, "Input string cannot be parsed as UNIX shell command") @@ -154,10 +155,10 @@ impl CliError { print_formatted_error(&mut stderr, "", e) } | CliError::Other(e) => { - print_formatted_error(&mut stderr, "Error: ", &e.to_string()) + print_anyhow_error(&mut stderr, "Error: ", e) } | CliError::OtherWithCode(e, _) => { - print_formatted_error(&mut stderr, "Error: ", &e.to_string()) + print_anyhow_error(&mut stderr, "Error: ", e) } | e @ CliError::InputString => { print_formatted_error(&mut stderr, "", &e.to_string()) @@ -220,6 +221,40 @@ fn print_formatted_error( Ok(()) } +fn print_anyhow_error( + f: &mut StandardStream, + heading: &str, + err: &anyhow::Error, +) -> std::io::Result<()> { + f.set_color(ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true))?; + write!(f, "{}", heading)?; + f.reset()?; + writeln!(f, "{}", err)?; + err.chain() + .skip(1) + .with_position() + .for_each(|(position, cause)| { + if position == Position::First { + let _ = + f.set_color(ColorSpec::new().set_fg(Some(Color::Magenta))); + let _ = writeln!(f, ""); + let _ = writeln!(f, "Caused by:"); + let _ = f.reset(); + } + let symbol = if position == Position::Last { + "└─" + } else { + "├─" + }; + let _ = + f.set_color(ColorSpec::new().set_italic(true).set_dimmed(true)); + let _ = write!(f, " {} ", symbol); + let _ = f.reset(); + let _ = writeln!(f, "{}", cause); + }); + Ok(()) +} + pub(crate) fn format_clap_error( err: clap::Error, ) -> clap::Error { diff --git a/examples/errors.rs b/examples/errors.rs new file mode 100644 index 0000000..25082ef --- /dev/null +++ b/examples/errors.rs @@ -0,0 +1,26 @@ +use cling::prelude::*; + +#[derive(Run, Parser, Debug, Clone)] +#[cling(run = "run")] +pub struct App {} + +// handlers can be sync or async, cling will handle this transparently. +async fn run() -> anyhow::Result<()> { + let err1 = std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "Fatal disk IO Error!", + ); + let err2 = anyhow::Error::new(err1).context("Trying to read a file"); + let err3 = err2.context("Can't load application"); + let err4 = err3.context("App level error"); + Err(err4) +} + +#[tokio::main] +async fn main() -> ClingFinished { + env_logger::builder().init(); + // Cling::parse().run().await + // Or, return ClingFinished to let cling handle error printing and exit + // code in a more convenient way. + Cling::parse_and_run().await +}