clap_verbosity/
lib.rs

1//! Control `log` level with a `--verbose` flag for your CLI
2//!
3//! This crate adds a `--verbose` flag to your CLI.  Most of its code
4//! came from [`clap_verbosity_flag`](https://crates.io/crates/clap_verbosity_flag) and
5//! as such, it is largely a superset and a drop-in replacement of that crate.
6//! 
7//! # Examples
8//!
9//! To get `--quiet` and `--verbose` flags through your entire program, just `flatten`
10//! [`Verbosity`]:
11//! ```rust,no_run
12//! # use clap::Parser;
13//! # use clap_verbosity::Verbosity;
14//! #
15//! # /// The CLI
16//! # #[derive(Debug, Parser)]
17//! # struct Cli {
18//! #[command(flatten)]
19//! verbose: Verbosity,
20//! # }
21//! ```
22//!
23//! You can then use this to configure your logger:
24//! ```rust,no_run
25//! # use clap::Parser;
26//! # use clap_verbosity::Verbosity;
27//! #
28//! # /// The CLI
29//! # #[derive(Debug, Parser)]
30//! # struct Cli {
31//! #     #[command(flatten)]
32//! #     verbose: Verbosity,
33//! # }
34//! let cli = Cli::parse();
35//! env_logger::Builder::new()
36//!     .filter_level(cli.verbose.log_level_filter())
37//!     .init();
38//! ```
39//!
40//! By default, this will only report errors.
41//! - `-q` silences output
42//! - `-v` show warnings
43//! - `-vv` show info
44//! - `-vvv` show debug
45//! - `-vvvv` show trace
46//!
47//! You can also customize the default logging level:
48//! ```rust,no_run
49//! # use clap::Parser;
50//! use clap_verbosity::{Verbosity, InfoLevel};
51//!
52//! /// The CLI
53//! #[derive(Debug, Parser)]
54//! struct Cli {
55//!     #[command(flatten)]
56//!     verbose: Verbosity<InfoLevel>,
57//! }
58//! ```
59//!
60//! Or implement [`LogLevel`] yourself for more control.
61
62#![cfg_attr(docsrs, feature(doc_auto_cfg))]
63
64#![cfg(feature = "serde")]
65pub mod serde_support;
66
67pub use log::Level;
68pub use log::LevelFilter;
69
70#[derive(clap::Args, Debug, Clone, Default)]
71pub struct Verbosity<L: LogLevel = ErrorLevel> {
72    #[arg(
73        long,
74        short = 'v',
75        action = clap::ArgAction::Count,
76        global = true,
77        help = L::verbose_help(),
78        long_help = L::verbose_long_help(),
79    )]
80    verbose: u8,
81
82    #[arg(
83        long,
84        short = 'q',
85        action = clap::ArgAction::Count,
86        global = true,
87        help = L::quiet_help(),
88        long_help = L::quiet_long_help(),
89        conflicts_with = "verbose",
90    )]
91    quiet: u8,
92
93    #[arg(skip)]
94    phantom: std::marker::PhantomData<L>,
95}
96
97impl<L: LogLevel> Verbosity<L> {
98    /// Create a new verbosity instance by explicitly setting the values
99    pub fn new(verbose: u8, quiet: u8) -> Self {
100        Verbosity {
101            verbose,
102            quiet,
103            phantom: std::marker::PhantomData,
104        }
105    }
106
107    /// Get the log level.
108    ///
109    /// `None` means all output is disabled.
110    pub fn log_level(&self) -> Option<log::Level> {
111        level_enum(self.verbosity())
112    }
113
114    /// Get the log level filter.
115    pub fn log_level_filter(&self) -> log::LevelFilter {
116        level_enum(self.verbosity())
117            .map(|l| l.to_level_filter())
118            .unwrap_or(log::LevelFilter::Off)
119    }
120
121    /// If the user requested complete silence (i.e. not just no-logging).
122    pub fn is_silent(&self) -> bool {
123        self.log_level().is_none()
124    }
125
126    fn verbosity(&self) -> i8 {
127        level_value(L::default()) - (self.quiet as i8) + (self.verbose as i8)
128    }
129}
130
131/// Converts an Option<log::Level> to an i8.
132pub fn level_value(level: Option<log::Level>) -> i8 {
133    match level {
134        None => -1,
135        Some(log::Level::Error) => 0,
136        Some(log::Level::Warn) => 1,
137        Some(log::Level::Info) => 2,
138        Some(log::Level::Debug) => 3,
139        Some(log::Level::Trace) => 4,
140    }
141}
142
143/// Converts an i8 to an Option<log::Level>.
144pub fn level_enum(verbosity: i8) -> Option<log::Level> {
145    match verbosity {
146        std::i8::MIN..=-1 => None,
147        0 => Some(log::Level::Error),
148        1 => Some(log::Level::Warn),
149        2 => Some(log::Level::Info),
150        3 => Some(log::Level::Debug),
151        4..=std::i8::MAX => Some(log::Level::Trace),
152    }
153}
154
155use std::fmt;
156
157impl<L: LogLevel> fmt::Display for Verbosity<L> {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        write!(f, "{}", self.verbosity())
160    }
161}
162
163pub trait LogLevel {
164    fn default() -> Option<log::Level>;
165
166    fn verbose_help() -> Option<&'static str> {
167        Some("More output per occurrence")
168    }
169
170    fn verbose_long_help() -> Option<&'static str> {
171        None
172    }
173
174    fn quiet_help() -> Option<&'static str> {
175        Some("Less output per occurrence")
176    }
177
178    fn quiet_long_help() -> Option<&'static str> {
179        None
180    }
181}
182
183#[derive(Copy, Clone, Debug, Default)]
184pub struct ErrorLevel;
185
186impl LogLevel for ErrorLevel {
187    fn default() -> Option<log::Level> {
188        Some(log::Level::Error)
189    }
190}
191
192#[derive(Copy, Clone, Debug, Default)]
193pub struct WarnLevel;
194
195impl LogLevel for WarnLevel {
196    fn default() -> Option<log::Level> {
197        Some(log::Level::Warn)
198    }
199}
200
201#[derive(Copy, Clone, Debug, Default)]
202pub struct InfoLevel;
203
204impl LogLevel for InfoLevel {
205    fn default() -> Option<log::Level> {
206        Some(log::Level::Info)
207    }
208}
209
210#[cfg(test)]
211mod test {
212    use super::*;
213
214    #[test]
215    fn verify_app() {
216        #[derive(Debug, clap::Parser)]
217        struct Cli {
218            #[command(flatten)]
219            verbose: Verbosity,
220        }
221
222        use clap::CommandFactory;
223        Cli::command().debug_assert()
224    }
225}