clap_verbosity_flag/
lib.rs

1//! Control `log` level with a `--verbose` flag for your CLI
2//!
3//! # Examples
4//!
5//! To get `--quiet` and `--verbose` flags through your entire program, just `flatten`
6//! [`Verbosity`]:
7//! ```rust,no_run
8//! # use clap::Parser;
9//! # use clap_verbosity_flag::Verbosity;
10//! #
11//! # /// Le CLI
12//! # #[derive(Debug, Parser)]
13//! # struct Cli {
14//! #[command(flatten)]
15//! verbose: Verbosity,
16//! # }
17//! ```
18//!
19//! You can then use this to configure your logger:
20//! ```rust,no_run
21//! # use clap::Parser;
22//! # use clap_verbosity_flag::Verbosity;
23//! #
24//! # /// Le CLI
25//! # #[derive(Debug, Parser)]
26//! # struct Cli {
27//! #     #[command(flatten)]
28//! #     verbose: Verbosity,
29//! # }
30//! let cli = Cli::parse();
31//! env_logger::Builder::new()
32//!     .filter_level(cli.verbose.log_level_filter())
33//!     .init();
34//! ```
35//!
36//! By default, this will only report errors.
37//! - `-q` silences output
38//! - `-v` show warnings
39//! - `-vv` show info
40//! - `-vvv` show debug
41//! - `-vvvv` show trace
42//!
43//! By default, the log level is set to Error. To customize this to a different level, pass a type
44//! implementing the [`LogLevel`] trait to [`Verbosity`]:
45//!
46//! ```rust,no_run
47//! # use clap::Parser;
48//! use clap_verbosity_flag::{Verbosity, InfoLevel};
49//!
50//! /// Le CLI
51//! #[derive(Debug, Parser)]
52//! struct Cli {
53//!     #[command(flatten)]
54//!     verbose: Verbosity<InfoLevel>,
55//! }
56//! ```
57//!
58//! Or implement our [`LogLevel`] trait to customize the default log level and help output.
59
60#![cfg_attr(docsrs, feature(doc_auto_cfg))]
61#![warn(clippy::print_stderr)]
62#![warn(clippy::print_stdout)]
63
64pub use log::Level;
65pub use log::LevelFilter;
66
67/// Logging flags to `#[command(flatten)]` into your CLI
68#[derive(clap::Args, Debug, Clone, Default)]
69#[command(about = None, long_about = None)]
70pub struct Verbosity<L: LogLevel = ErrorLevel> {
71    #[arg(
72        long,
73        short = 'v',
74        action = clap::ArgAction::Count,
75        global = true,
76        help = L::verbose_help(),
77        long_help = L::verbose_long_help(),
78    )]
79    verbose: u8,
80
81    #[arg(
82        long,
83        short = 'q',
84        action = clap::ArgAction::Count,
85        global = true,
86        help = L::quiet_help(),
87        long_help = L::quiet_long_help(),
88        conflicts_with = "verbose",
89    )]
90    quiet: u8,
91
92    #[arg(skip)]
93    phantom: std::marker::PhantomData<L>,
94}
95
96impl<L: LogLevel> Verbosity<L> {
97    /// Create a new verbosity instance by explicitly setting the values
98    pub fn new(verbose: u8, quiet: u8) -> Self {
99        Verbosity {
100            verbose,
101            quiet,
102            phantom: std::marker::PhantomData,
103        }
104    }
105
106    /// Whether any verbosity flags (either `--verbose` or `--quiet`)
107    /// are present on the command line.
108    pub fn is_present(&self) -> bool {
109        self.verbose != 0 || self.quiet != 0
110    }
111
112    /// Get the log level.
113    ///
114    /// `None` means all output is disabled.
115    pub fn log_level(&self) -> Option<Level> {
116        level_enum(self.verbosity())
117    }
118
119    /// Get the log level filter.
120    pub fn log_level_filter(&self) -> LevelFilter {
121        level_enum(self.verbosity())
122            .map(|l| l.to_level_filter())
123            .unwrap_or(LevelFilter::Off)
124    }
125
126    /// If the user requested complete silence (i.e. not just no-logging).
127    pub fn is_silent(&self) -> bool {
128        self.log_level().is_none()
129    }
130
131    fn verbosity(&self) -> u8 {
132        let default_verbosity = level_value(L::default());
133        let verbosity = default_verbosity as i16 - self.quiet as i16 + self.verbose as i16;
134        verbosity.clamp(0, u8::MAX as i16) as u8
135    }
136}
137
138fn level_value(level: Option<Level>) -> u8 {
139    match level {
140        None => 0,
141        Some(Level::Error) => 1,
142        Some(Level::Warn) => 2,
143        Some(Level::Info) => 3,
144        Some(Level::Debug) => 4,
145        Some(Level::Trace) => 5,
146    }
147}
148
149fn level_enum(verbosity: u8) -> Option<Level> {
150    match verbosity {
151        0 => None,
152        1 => Some(Level::Error),
153        2 => Some(Level::Warn),
154        3 => Some(Level::Info),
155        4 => Some(Level::Debug),
156        5..=u8::MAX => Some(Level::Trace),
157    }
158}
159
160use std::fmt;
161
162impl<L: LogLevel> fmt::Display for Verbosity<L> {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        write!(f, "{}", self.verbosity())
165    }
166}
167
168/// Customize the default log-level and associated help
169pub trait LogLevel {
170    /// Base-line level before applying `--verbose` and `--quiet`
171    fn default() -> Option<Level>;
172
173    /// Short-help message for `--verbose`
174    fn verbose_help() -> Option<&'static str> {
175        Some("Increase logging verbosity")
176    }
177
178    /// Long-help message for `--verbose`
179    fn verbose_long_help() -> Option<&'static str> {
180        None
181    }
182
183    /// Short-help message for `--quiet`
184    fn quiet_help() -> Option<&'static str> {
185        Some("Decrease logging verbosity")
186    }
187
188    /// Long-help message for `--quiet`
189    fn quiet_long_help() -> Option<&'static str> {
190        None
191    }
192}
193
194/// Default to [`log::Level::Error`]
195#[derive(Copy, Clone, Debug, Default)]
196pub struct ErrorLevel;
197
198impl LogLevel for ErrorLevel {
199    fn default() -> Option<Level> {
200        Some(Level::Error)
201    }
202}
203
204/// Default to [`log::Level::Warn`]
205#[derive(Copy, Clone, Debug, Default)]
206pub struct WarnLevel;
207
208impl LogLevel for WarnLevel {
209    fn default() -> Option<Level> {
210        Some(Level::Warn)
211    }
212}
213
214/// Default to [`log::Level::Info`]
215#[derive(Copy, Clone, Debug, Default)]
216pub struct InfoLevel;
217
218impl LogLevel for InfoLevel {
219    fn default() -> Option<Level> {
220        Some(Level::Info)
221    }
222}
223
224/// Default to [`log::Level::Debug`]
225#[derive(Copy, Clone, Debug, Default)]
226pub struct DebugLevel;
227
228impl LogLevel for DebugLevel {
229    fn default() -> Option<Level> {
230        Some(Level::Debug)
231    }
232}
233
234/// Default to [`log::Level::Trace`]
235#[derive(Copy, Clone, Debug, Default)]
236pub struct TraceLevel;
237
238impl LogLevel for TraceLevel {
239    fn default() -> Option<Level> {
240        Some(Level::Trace)
241    }
242}
243
244/// Default to no logging (i.e. `None` or [`log::LevelFilter::Off`])
245#[derive(Copy, Clone, Debug, Default)]
246pub struct OffLevel;
247
248impl LogLevel for OffLevel {
249    fn default() -> Option<Level> {
250        None
251    }
252}
253
254#[cfg(test)]
255mod test {
256    use super::*;
257
258    #[test]
259    fn verify_app() {
260        #[derive(Debug, clap::Parser)]
261        struct Cli {
262            #[command(flatten)]
263            verbose: Verbosity,
264        }
265
266        use clap::CommandFactory;
267        Cli::command().debug_assert();
268    }
269
270    #[test]
271    fn verbosity_off_level() {
272        let tests = [
273            // verbose, quiet, expected_level, expected_filter
274            (0, 0, None, LevelFilter::Off),
275            (1, 0, Some(Level::Error), LevelFilter::Error),
276            (2, 0, Some(Level::Warn), LevelFilter::Warn),
277            (3, 0, Some(Level::Info), LevelFilter::Info),
278            (4, 0, Some(Level::Debug), LevelFilter::Debug),
279            (5, 0, Some(Level::Trace), LevelFilter::Trace),
280            (6, 0, Some(Level::Trace), LevelFilter::Trace),
281            (255, 0, Some(Level::Trace), LevelFilter::Trace),
282            (0, 1, None, LevelFilter::Off),
283            (0, 255, None, LevelFilter::Off),
284            (255, 255, None, LevelFilter::Off),
285        ];
286
287        for (verbose, quiet, expected_level, expected_filter) in tests.iter() {
288            let v = Verbosity::<OffLevel>::new(*verbose, *quiet);
289            assert_eq!(
290                v.log_level(),
291                *expected_level,
292                "verbose = {verbose}, quiet = {quiet}"
293            );
294            assert_eq!(
295                v.log_level_filter(),
296                *expected_filter,
297                "verbose = {verbose}, quiet = {quiet}"
298            );
299        }
300    }
301
302    #[test]
303    fn verbosity_error_level() {
304        let tests = [
305            // verbose, quiet, expected_level, expected_filter
306            (0, 0, Some(Level::Error), LevelFilter::Error),
307            (1, 0, Some(Level::Warn), LevelFilter::Warn),
308            (2, 0, Some(Level::Info), LevelFilter::Info),
309            (3, 0, Some(Level::Debug), LevelFilter::Debug),
310            (4, 0, Some(Level::Trace), LevelFilter::Trace),
311            (5, 0, Some(Level::Trace), LevelFilter::Trace),
312            (255, 0, Some(Level::Trace), LevelFilter::Trace),
313            (0, 1, None, LevelFilter::Off),
314            (0, 2, None, LevelFilter::Off),
315            (0, 255, None, LevelFilter::Off),
316            (255, 255, Some(Level::Error), LevelFilter::Error),
317        ];
318
319        for (verbose, quiet, expected_level, expected_filter) in tests.iter() {
320            let v = Verbosity::<ErrorLevel>::new(*verbose, *quiet);
321            assert_eq!(
322                v.log_level(),
323                *expected_level,
324                "verbose = {verbose}, quiet = {quiet}"
325            );
326            assert_eq!(
327                v.log_level_filter(),
328                *expected_filter,
329                "verbose = {verbose}, quiet = {quiet}"
330            );
331        }
332    }
333
334    #[test]
335    fn verbosity_warn_level() {
336        let tests = [
337            // verbose, quiet, expected_level, expected_filter
338            (0, 0, Some(Level::Warn), LevelFilter::Warn),
339            (1, 0, Some(Level::Info), LevelFilter::Info),
340            (2, 0, Some(Level::Debug), LevelFilter::Debug),
341            (3, 0, Some(Level::Trace), LevelFilter::Trace),
342            (4, 0, Some(Level::Trace), LevelFilter::Trace),
343            (255, 0, Some(Level::Trace), LevelFilter::Trace),
344            (0, 1, Some(Level::Error), LevelFilter::Error),
345            (0, 2, None, LevelFilter::Off),
346            (0, 3, None, LevelFilter::Off),
347            (0, 255, None, LevelFilter::Off),
348            (255, 255, Some(Level::Warn), LevelFilter::Warn),
349        ];
350
351        for (verbose, quiet, expected_level, expected_filter) in tests.iter() {
352            let v = Verbosity::<WarnLevel>::new(*verbose, *quiet);
353            assert_eq!(
354                v.log_level(),
355                *expected_level,
356                "verbose = {verbose}, quiet = {quiet}"
357            );
358            assert_eq!(
359                v.log_level_filter(),
360                *expected_filter,
361                "verbose = {verbose}, quiet = {quiet}"
362            );
363        }
364    }
365
366    #[test]
367    fn verbosity_info_level() {
368        let tests = [
369            // verbose, quiet, expected_level, expected_filter
370            (0, 0, Some(Level::Info), LevelFilter::Info),
371            (1, 0, Some(Level::Debug), LevelFilter::Debug),
372            (2, 0, Some(Level::Trace), LevelFilter::Trace),
373            (3, 0, Some(Level::Trace), LevelFilter::Trace),
374            (255, 0, Some(Level::Trace), LevelFilter::Trace),
375            (0, 1, Some(Level::Warn), LevelFilter::Warn),
376            (0, 2, Some(Level::Error), LevelFilter::Error),
377            (0, 3, None, LevelFilter::Off),
378            (0, 4, None, LevelFilter::Off),
379            (0, 255, None, LevelFilter::Off),
380            (255, 255, Some(Level::Info), LevelFilter::Info),
381        ];
382
383        for (verbose, quiet, expected_level, expected_filter) in tests.iter() {
384            let v = Verbosity::<InfoLevel>::new(*verbose, *quiet);
385            assert_eq!(
386                v.log_level(),
387                *expected_level,
388                "verbose = {verbose}, quiet = {quiet}"
389            );
390            assert_eq!(
391                v.log_level_filter(),
392                *expected_filter,
393                "verbose = {verbose}, quiet = {quiet}"
394            );
395        }
396    }
397
398    #[test]
399    fn verbosity_debug_level() {
400        let tests = [
401            // verbose, quiet, expected_level, expected_filter
402            (0, 0, Some(Level::Debug), LevelFilter::Debug),
403            (1, 0, Some(Level::Trace), LevelFilter::Trace),
404            (2, 0, Some(Level::Trace), LevelFilter::Trace),
405            (255, 0, Some(Level::Trace), LevelFilter::Trace),
406            (0, 1, Some(Level::Info), LevelFilter::Info),
407            (0, 2, Some(Level::Warn), LevelFilter::Warn),
408            (0, 3, Some(Level::Error), LevelFilter::Error),
409            (0, 4, None, LevelFilter::Off),
410            (0, 5, None, LevelFilter::Off),
411            (0, 255, None, LevelFilter::Off),
412            (255, 255, Some(Level::Debug), LevelFilter::Debug),
413        ];
414
415        for (verbose, quiet, expected_level, expected_filter) in tests.iter() {
416            let v = Verbosity::<DebugLevel>::new(*verbose, *quiet);
417            assert_eq!(
418                v.log_level(),
419                *expected_level,
420                "verbose = {verbose}, quiet = {quiet}"
421            );
422            assert_eq!(
423                v.log_level_filter(),
424                *expected_filter,
425                "verbose = {verbose}, quiet = {quiet}"
426            );
427        }
428    }
429
430    #[test]
431    fn verbosity_trace_level() {
432        let tests = [
433            // verbose, quiet, expected_level, expected_filter
434            (0, 0, Some(Level::Trace), LevelFilter::Trace),
435            (1, 0, Some(Level::Trace), LevelFilter::Trace),
436            (255, 0, Some(Level::Trace), LevelFilter::Trace),
437            (0, 1, Some(Level::Debug), LevelFilter::Debug),
438            (0, 2, Some(Level::Info), LevelFilter::Info),
439            (0, 3, Some(Level::Warn), LevelFilter::Warn),
440            (0, 4, Some(Level::Error), LevelFilter::Error),
441            (0, 5, None, LevelFilter::Off),
442            (0, 6, None, LevelFilter::Off),
443            (0, 255, None, LevelFilter::Off),
444            (255, 255, Some(Level::Trace), LevelFilter::Trace),
445        ];
446
447        for (verbose, quiet, expected_level, expected_filter) in tests.iter() {
448            let v = Verbosity::<TraceLevel>::new(*verbose, *quiet);
449            assert_eq!(
450                v.log_level(),
451                *expected_level,
452                "verbose = {verbose}, quiet = {quiet}"
453            );
454            assert_eq!(
455                v.log_level_filter(),
456                *expected_filter,
457                "verbose = {verbose}, quiet = {quiet}"
458            );
459        }
460    }
461}