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