clap_verbosity_flag/
lib.rs

1//! Easily add `--verbose` and `--quiet` flags to CLIs using [Clap](http://crates.io/crates/clap).
2//!
3//! # Examples
4//!
5//! To get `--quiet` and `--verbose` flags through your entire program, just `flatten`
6//! [`Verbosity`]:
7//!
8//! ```rust,no_run
9//! use clap::Parser;
10//! use clap_verbosity_flag::Verbosity;
11//!
12//! #[derive(Debug, Parser)]
13//! struct Cli {
14//!     #[command(flatten)]
15//!     verbosity: Verbosity,
16//!
17//!     // ... other options
18//! }
19//! ```
20//!
21//! You can then use this to configure your logger:
22//!
23//! ```rust,no_run
24//! # use clap::Parser;
25//! # use clap_verbosity_flag::Verbosity;
26//! #
27//! # #[derive(Debug, Parser)]
28//! # struct Cli {
29//! #     #[command(flatten)]
30//! #     verbosity: Verbosity,
31//! # }
32//! let cli = Cli::parse();
33//! # #[cfg(feature = "log")]
34//! env_logger::Builder::new()
35//!     .filter_level(cli.verbosity.log_level_filter())
36//!     .init();
37//! ```
38//!
39//! ## Use with `tracing`
40//!
41//! To use with [`tracing`](https://crates.io/crates/tracing), disable the log feature flag and
42//! enable the `tracing` feature flag:
43//!
44//! ```shell
45//! cargo add clap_verbosity_flag --no-default features --features tracing
46//! ```
47//!
48//! Then you can use it like this:
49//!
50//! ```rust,no_run
51//! # use clap::Parser;
52//! # use clap_verbosity_flag::Verbosity;
53//! #
54//! # #[derive(Debug, Parser)]
55//! # struct Cli {
56//! #     #[command(flatten)]
57//! #     verbosity: Verbosity,
58//! # }
59//! let cli = Cli::parse();
60//! # #[cfg(feature = "tracing")]
61//! tracing_subscriber::fmt()
62//!     .with_max_level(cli.verbosity)
63//!     .init();
64//! ```
65//!
66//! # Using `--verbose` and `--quiet` flags
67//!
68//! The default verbosity level will cause `log` / `tracing` to only report errors. The flags can be
69//!  specified multiple times to increase or decrease the verbosity level.
70//!
71//! - silence output: `-q` / `--quiet`
72//! - show warnings: `-v` / `--verbose`
73//! - show info: `-vv` / `--verbose --verbose`
74//! - show debug: `-vvv` / `--verbose --verbose --verbose`
75//! - show trace: `-vvvv` / `--verbose --verbose --verbose --verbose`
76//!
77//! # Customizing the default log level
78//!
79//! By default, the log level is set to Error. To customize this to a different level, pass a type
80//! implementing the [`LogLevel`] trait to [`Verbosity`]:
81//!
82//! ```rust,no_run
83//! # use clap::Parser;
84//! use clap_verbosity_flag::{Verbosity, InfoLevel};
85//!
86//! #[derive(Debug, Parser)]
87//! struct Cli {
88//!     #[command(flatten)]
89//!     verbose: Verbosity<InfoLevel>,
90//! }
91//! ```
92//!
93//! Or implement our [`LogLevel`] trait to customize the default log level and help output.
94
95#![cfg_attr(docsrs, feature(doc_auto_cfg))]
96#![warn(clippy::print_stderr)]
97#![warn(clippy::print_stdout)]
98
99#[doc = include_str!("../README.md")]
100#[cfg(all(doctest, feature = "log", feature = "tracing"))]
101pub struct ReadmeDoctests;
102
103use std::fmt;
104
105#[cfg(feature = "log")]
106pub mod log;
107#[cfg(feature = "tracing")]
108pub mod tracing;
109
110/// Logging flags to `#[command(flatten)]` into your CLI
111#[derive(clap::Args, Debug, Clone, Copy, Default, PartialEq, Eq)]
112#[command(about = None, long_about = None)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
114#[cfg_attr(
115    feature = "serde",
116    serde(
117        from = "VerbosityFilter",
118        into = "VerbosityFilter",
119        bound(serialize = "L: Clone")
120    )
121)]
122#[cfg_attr(
123    feature = "serde",
124    doc = r#"This type serializes to a string representation of the log level, e.g. `"Debug"`"#
125)]
126pub struct Verbosity<L: LogLevel = ErrorLevel> {
127    #[arg(
128        long,
129        short = 'v',
130        action = clap::ArgAction::Count,
131        global = true,
132        help = L::verbose_help(),
133        long_help = L::verbose_long_help(),
134    )]
135    verbose: u8,
136
137    #[arg(
138        long,
139        short = 'q',
140        action = clap::ArgAction::Count,
141        global = true,
142        help = L::quiet_help(),
143        long_help = L::quiet_long_help(),
144        conflicts_with = "verbose",
145    )]
146    quiet: u8,
147
148    #[arg(skip)]
149    phantom: std::marker::PhantomData<L>,
150}
151
152impl<L: LogLevel> Verbosity<L> {
153    /// Create a new verbosity instance by explicitly setting the values
154    pub fn new(verbose: u8, quiet: u8) -> Self {
155        Verbosity {
156            verbose,
157            quiet,
158            phantom: std::marker::PhantomData,
159        }
160    }
161
162    /// Whether any verbosity flags (either `--verbose` or `--quiet`)
163    /// are present on the command line.
164    pub fn is_present(&self) -> bool {
165        self.verbose != 0 || self.quiet != 0
166    }
167
168    /// If the user requested complete silence (i.e. not just no-logging).
169    pub fn is_silent(&self) -> bool {
170        self.filter() == VerbosityFilter::Off
171    }
172
173    /// Gets the filter that should be applied to the logger.
174    pub fn filter(&self) -> VerbosityFilter {
175        let offset = self.verbose as i16 - self.quiet as i16;
176        L::default_filter().with_offset(offset)
177    }
178}
179
180#[cfg(feature = "log")]
181impl<L: LogLevel> Verbosity<L> {
182    /// Get the log level.
183    ///
184    /// `None` means all output is disabled.
185    pub fn log_level(&self) -> Option<log::Level> {
186        self.filter().into()
187    }
188
189    /// Get the log level filter.
190    pub fn log_level_filter(&self) -> log::LevelFilter {
191        self.filter().into()
192    }
193}
194
195#[cfg(feature = "tracing")]
196impl<L: LogLevel> Verbosity<L> {
197    /// Get the tracing level.
198    ///
199    /// `None` means all output is disabled.
200    pub fn tracing_level(&self) -> Option<tracing_core::Level> {
201        self.filter().into()
202    }
203
204    /// Get the tracing level filter.
205    pub fn tracing_level_filter(&self) -> tracing_core::LevelFilter {
206        self.filter().into()
207    }
208}
209
210impl<L: LogLevel> fmt::Display for Verbosity<L> {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        self.filter().fmt(f)
213    }
214}
215
216impl<L: LogLevel> From<Verbosity<L>> for VerbosityFilter {
217    fn from(verbosity: Verbosity<L>) -> Self {
218        verbosity.filter()
219    }
220}
221
222impl<L: LogLevel> From<VerbosityFilter> for Verbosity<L> {
223    fn from(filter: VerbosityFilter) -> Self {
224        let default = L::default_filter();
225        let verbose = filter.value().saturating_sub(default.value());
226        let quiet = default.value().saturating_sub(filter.value());
227        Verbosity::new(verbose, quiet)
228    }
229}
230
231/// Customize the default log-level and associated help
232pub trait LogLevel {
233    /// Baseline level before applying `--verbose` and `--quiet`
234    fn default_filter() -> VerbosityFilter;
235
236    /// Short-help message for `--verbose`
237    fn verbose_help() -> Option<&'static str> {
238        Some("Increase logging verbosity")
239    }
240
241    /// Long-help message for `--verbose`
242    fn verbose_long_help() -> Option<&'static str> {
243        None
244    }
245
246    /// Short-help message for `--quiet`
247    fn quiet_help() -> Option<&'static str> {
248        Some("Decrease logging verbosity")
249    }
250
251    /// Long-help message for `--quiet`
252    fn quiet_long_help() -> Option<&'static str> {
253        None
254    }
255}
256
257/// A representation of the log level filter.
258///
259/// Used to calculate the log level and filter.
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
262#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
263pub enum VerbosityFilter {
264    Off,
265    Error,
266    Warn,
267    Info,
268    Debug,
269    Trace,
270}
271
272impl VerbosityFilter {
273    /// Apply an offset to the filter level.
274    ///
275    /// Negative values will decrease the verbosity, while positive values will increase it.
276    fn with_offset(&self, offset: i16) -> VerbosityFilter {
277        match i16::from(self.value()).saturating_add(offset) {
278            i16::MIN..=0 => Self::Off,
279            1 => Self::Error,
280            2 => Self::Warn,
281            3 => Self::Info,
282            4 => Self::Debug,
283            5..=i16::MAX => Self::Trace,
284        }
285    }
286
287    /// Get the numeric value of the filter.
288    ///
289    /// This is an internal representation of the filter level used only for conversion / offset.
290    fn value(&self) -> u8 {
291        match self {
292            Self::Off => 0,
293            Self::Error => 1,
294            Self::Warn => 2,
295            Self::Info => 3,
296            Self::Debug => 4,
297            Self::Trace => 5,
298        }
299    }
300}
301
302impl fmt::Display for VerbosityFilter {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        match self {
305            Self::Off => write!(f, "off"),
306            Self::Error => write!(f, "error"),
307            Self::Warn => write!(f, "warn"),
308            Self::Info => write!(f, "info"),
309            Self::Debug => write!(f, "debug"),
310            Self::Trace => write!(f, "trace"),
311        }
312    }
313}
314
315/// Default to [`VerbosityFilter::Error`]
316#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
317pub struct ErrorLevel;
318
319impl LogLevel for ErrorLevel {
320    fn default_filter() -> VerbosityFilter {
321        VerbosityFilter::Error
322    }
323}
324
325/// Default to [`VerbosityFilter::Warn`]
326#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
327pub struct WarnLevel;
328
329impl LogLevel for WarnLevel {
330    fn default_filter() -> VerbosityFilter {
331        VerbosityFilter::Warn
332    }
333}
334
335/// Default to [`VerbosityFilter::Info`]
336#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
337pub struct InfoLevel;
338
339impl LogLevel for InfoLevel {
340    fn default_filter() -> VerbosityFilter {
341        VerbosityFilter::Info
342    }
343}
344
345/// Default to [`VerbosityFilter::Debug`]
346#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
347pub struct DebugLevel;
348
349impl LogLevel for DebugLevel {
350    fn default_filter() -> VerbosityFilter {
351        VerbosityFilter::Debug
352    }
353}
354
355/// Default to [`VerbosityFilter::Trace`]
356#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
357pub struct TraceLevel;
358
359impl LogLevel for TraceLevel {
360    fn default_filter() -> VerbosityFilter {
361        VerbosityFilter::Trace
362    }
363}
364
365/// Default to [`VerbosityFilter::Off`] (no logging)
366#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
367pub struct OffLevel;
368
369impl LogLevel for OffLevel {
370    fn default_filter() -> VerbosityFilter {
371        VerbosityFilter::Off
372    }
373}
374
375#[cfg(test)]
376mod test {
377    use super::*;
378
379    #[test]
380    fn verify_app() {
381        #[derive(Debug, clap::Parser)]
382        struct Cli {
383            #[command(flatten)]
384            verbose: Verbosity,
385        }
386
387        use clap::CommandFactory;
388        Cli::command().debug_assert();
389    }
390
391    /// Asserts that the filter is correct for the given verbosity and quiet values.
392    #[track_caller]
393    fn assert_filter<L: LogLevel>(verbose: u8, quiet: u8, expected: VerbosityFilter) {
394        assert_eq!(
395            Verbosity::<L>::new(verbose, quiet).filter(),
396            expected,
397            "verbose = {verbose}, quiet = {quiet}"
398        );
399    }
400
401    #[test]
402    fn verbosity_off_level() {
403        let tests = [
404            (0, 0, VerbosityFilter::Off),
405            (1, 0, VerbosityFilter::Error),
406            (2, 0, VerbosityFilter::Warn),
407            (3, 0, VerbosityFilter::Info),
408            (4, 0, VerbosityFilter::Debug),
409            (5, 0, VerbosityFilter::Trace),
410            (6, 0, VerbosityFilter::Trace),
411            (255, 0, VerbosityFilter::Trace),
412            (0, 1, VerbosityFilter::Off),
413            (0, 255, VerbosityFilter::Off),
414            (255, 255, VerbosityFilter::Off),
415        ];
416
417        for (verbose, quiet, expected_filter) in tests {
418            assert_filter::<OffLevel>(verbose, quiet, expected_filter);
419        }
420    }
421
422    #[test]
423    fn verbosity_error_level() {
424        let tests = [
425            (0, 0, VerbosityFilter::Error),
426            (1, 0, VerbosityFilter::Warn),
427            (2, 0, VerbosityFilter::Info),
428            (3, 0, VerbosityFilter::Debug),
429            (4, 0, VerbosityFilter::Trace),
430            (5, 0, VerbosityFilter::Trace),
431            (255, 0, VerbosityFilter::Trace),
432            (0, 1, VerbosityFilter::Off),
433            (0, 2, VerbosityFilter::Off),
434            (0, 255, VerbosityFilter::Off),
435            (255, 255, VerbosityFilter::Error),
436        ];
437
438        for (verbose, quiet, expected_filter) in tests {
439            assert_filter::<ErrorLevel>(verbose, quiet, expected_filter);
440        }
441    }
442
443    #[test]
444    fn verbosity_warn_level() {
445        let tests = [
446            // verbose, quiet, expected_level, expected_filter
447            (0, 0, VerbosityFilter::Warn),
448            (1, 0, VerbosityFilter::Info),
449            (2, 0, VerbosityFilter::Debug),
450            (3, 0, VerbosityFilter::Trace),
451            (4, 0, VerbosityFilter::Trace),
452            (255, 0, VerbosityFilter::Trace),
453            (0, 1, VerbosityFilter::Error),
454            (0, 2, VerbosityFilter::Off),
455            (0, 3, VerbosityFilter::Off),
456            (0, 255, VerbosityFilter::Off),
457            (255, 255, VerbosityFilter::Warn),
458        ];
459
460        for (verbose, quiet, expected_filter) in tests {
461            assert_filter::<WarnLevel>(verbose, quiet, expected_filter);
462        }
463    }
464
465    #[test]
466    fn verbosity_info_level() {
467        let tests = [
468            // verbose, quiet, expected_level, expected_filter
469            (0, 0, VerbosityFilter::Info),
470            (1, 0, VerbosityFilter::Debug),
471            (2, 0, VerbosityFilter::Trace),
472            (3, 0, VerbosityFilter::Trace),
473            (255, 0, VerbosityFilter::Trace),
474            (0, 1, VerbosityFilter::Warn),
475            (0, 2, VerbosityFilter::Error),
476            (0, 3, VerbosityFilter::Off),
477            (0, 4, VerbosityFilter::Off),
478            (0, 255, VerbosityFilter::Off),
479            (255, 255, VerbosityFilter::Info),
480        ];
481
482        for (verbose, quiet, expected_filter) in tests {
483            assert_filter::<InfoLevel>(verbose, quiet, expected_filter);
484        }
485    }
486
487    #[test]
488    fn verbosity_debug_level() {
489        let tests = [
490            // verbose, quiet, expected_level, expected_filter
491            (0, 0, VerbosityFilter::Debug),
492            (1, 0, VerbosityFilter::Trace),
493            (2, 0, VerbosityFilter::Trace),
494            (255, 0, VerbosityFilter::Trace),
495            (0, 1, VerbosityFilter::Info),
496            (0, 2, VerbosityFilter::Warn),
497            (0, 3, VerbosityFilter::Error),
498            (0, 4, VerbosityFilter::Off),
499            (0, 5, VerbosityFilter::Off),
500            (0, 255, VerbosityFilter::Off),
501            (255, 255, VerbosityFilter::Debug),
502        ];
503
504        for (verbose, quiet, expected_filter) in tests {
505            assert_filter::<DebugLevel>(verbose, quiet, expected_filter);
506        }
507    }
508
509    #[test]
510    fn verbosity_trace_level() {
511        let tests = [
512            // verbose, quiet, expected_level, expected_filter
513            (0, 0, VerbosityFilter::Trace),
514            (1, 0, VerbosityFilter::Trace),
515            (255, 0, VerbosityFilter::Trace),
516            (0, 1, VerbosityFilter::Debug),
517            (0, 2, VerbosityFilter::Info),
518            (0, 3, VerbosityFilter::Warn),
519            (0, 4, VerbosityFilter::Error),
520            (0, 5, VerbosityFilter::Off),
521            (0, 6, VerbosityFilter::Off),
522            (0, 255, VerbosityFilter::Off),
523            (255, 255, VerbosityFilter::Trace),
524        ];
525
526        for (verbose, quiet, expected_filter) in tests {
527            assert_filter::<TraceLevel>(verbose, quiet, expected_filter);
528        }
529    }
530
531    #[test]
532    fn from_verbosity_filter() {
533        for &filter in &[
534            VerbosityFilter::Off,
535            VerbosityFilter::Error,
536            VerbosityFilter::Warn,
537            VerbosityFilter::Info,
538            VerbosityFilter::Debug,
539            VerbosityFilter::Trace,
540        ] {
541            assert_eq!(Verbosity::<OffLevel>::from(filter).filter(), filter);
542            assert_eq!(Verbosity::<ErrorLevel>::from(filter).filter(), filter);
543            assert_eq!(Verbosity::<WarnLevel>::from(filter).filter(), filter);
544            assert_eq!(Verbosity::<InfoLevel>::from(filter).filter(), filter);
545            assert_eq!(Verbosity::<DebugLevel>::from(filter).filter(), filter);
546            assert_eq!(Verbosity::<TraceLevel>::from(filter).filter(), filter);
547        }
548    }
549}
550
551#[cfg(feature = "serde")]
552#[cfg(test)]
553mod serde_tests {
554    use super::*;
555
556    use clap::Parser;
557    use serde::{Deserialize, Serialize};
558
559    #[derive(Debug, Parser, Serialize, Deserialize)]
560    struct Cli {
561        meaning_of_life: u8,
562        #[command(flatten)]
563        verbosity: Verbosity<InfoLevel>,
564    }
565
566    #[test]
567    fn serialize_toml() {
568        let cli = Cli {
569            meaning_of_life: 42,
570            verbosity: Verbosity::new(2, 1),
571        };
572        let toml = toml::to_string(&cli).unwrap();
573        assert_eq!(toml, "meaning_of_life = 42\nverbosity = \"debug\"\n");
574    }
575
576    #[test]
577    fn deserialize_toml() {
578        let toml = "meaning_of_life = 42\nverbosity = \"debug\"\n";
579        let cli: Cli = toml::from_str(toml).unwrap();
580        assert_eq!(cli.meaning_of_life, 42);
581        assert_eq!(cli.verbosity.filter(), VerbosityFilter::Debug);
582    }
583
584    /// Tests that the `Verbosity` can be serialized and deserialized correctly from an a token.
585    #[test]
586    fn serde_round_trips() {
587        use serde_test::{assert_tokens, Token};
588
589        for (filter, variant) in [
590            (VerbosityFilter::Off, "off"),
591            (VerbosityFilter::Error, "error"),
592            (VerbosityFilter::Warn, "warn"),
593            (VerbosityFilter::Info, "info"),
594            (VerbosityFilter::Debug, "debug"),
595            (VerbosityFilter::Trace, "trace"),
596        ] {
597            let tokens = &[Token::UnitVariant {
598                name: "VerbosityFilter",
599                variant,
600            }];
601
602            // `assert_tokens` checks both serialization and deserialization.
603            assert_tokens(&Verbosity::<OffLevel>::from(filter), tokens);
604            assert_tokens(&Verbosity::<ErrorLevel>::from(filter), tokens);
605            assert_tokens(&Verbosity::<WarnLevel>::from(filter), tokens);
606            assert_tokens(&Verbosity::<InfoLevel>::from(filter), tokens);
607            assert_tokens(&Verbosity::<DebugLevel>::from(filter), tokens);
608            assert_tokens(&Verbosity::<TraceLevel>::from(filter), tokens);
609        }
610    }
611}