clap_logflag/
clap.rs

1use clap::Parser;
2
3use crate::{LogDestinationConfig, LoggingConfig};
4
5// We need to remove doc comments here, otherwise clap adds them to the help message
6#[allow(missing_docs)]
7#[derive(Parser, Debug)]
8pub struct LogArgs {
9    /// Log definition consisting of an optional log level filter, and a log destination.
10    /// You can define this argument multiple times for multiple log destinations.
11    ///
12    /// Logging can be disabled with `--log none`.
13    /// If combined with other log definitions, those will take precedence and logging will not be disabled.
14    ///
15    /// The argument can be combined with a level filter to only log messages of a certain level or higher to that destination.
16    ///
17    /// Format: destination | level_filter:destination
18    /// * level_filter = "ERROR" | "WARN" | "INFO" | "DEBUG" | "TRACE"
19    /// * destination = "stderr" | "syslog" | "file:path" | "none"
20    ///
21    /// Examples:
22    /// * `--log syslog`
23    /// * `--log stderr`
24    /// * `--log file:/path/to/file`
25    /// * `--log INFO:stderr`
26    /// * `--log DEBUG:file:/path/to/file`
27    /// * `--log TRACE:syslog`
28    /// * `--log none`
29    #[arg(long, value_parser=parse_destination_config)]
30    #[clap(verbatim_doc_comment)]
31    pub log: Vec<Option<LogDestinationConfig>>,
32}
33
34fn parse_destination_config(input: &str) -> Result<Option<LogDestinationConfig>, String> {
35    crate::parser::parse_config_definition(input).map_err(|err| err.to_string())
36}
37
38impl LogArgs {
39    /// Build the [LoggingConfig] defined by the command line arguments from [LogArgs].
40    /// If no `--log` argument is given, the default config is returned.
41    pub fn or_default(&self, default: LoggingConfig) -> LoggingConfig {
42        if self.log.is_empty() {
43            // No `--log` argument given, use the default config
44            default
45        } else {
46            // There are `--log` arguments given, but they may be `--log none`.
47            // Let's filter those out. If no non-none are remaining, logging will be disabled.
48            let destinations = self.log.iter().filter_map(|log| log.clone()).collect();
49            LoggingConfig::new(destinations)
50        }
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    mod parse_destination_config {
59        use crate::LogDestination;
60
61        use super::*;
62
63        #[test]
64        fn empty_string() {
65            assert_eq!(
66                parse_destination_config(""),
67                Err(
68                    "Invalid empty log destination. Choose stderr, syslog, file, or none"
69                        .to_string()
70                )
71            );
72        }
73
74        #[test]
75        fn none() {
76            assert_eq!(parse_destination_config("none"), Ok(None));
77        }
78
79        #[test]
80        fn stderr() {
81            assert_eq!(
82                parse_destination_config("stderr"),
83                Ok(Some(LogDestinationConfig {
84                    destination: LogDestination::Stderr,
85                    level: None
86                }))
87            );
88        }
89
90        #[test]
91        fn stderr_with_level() {
92            assert_eq!(
93                parse_destination_config("DEBUG:stderr"),
94                Ok(Some(LogDestinationConfig {
95                    destination: LogDestination::Stderr,
96                    level: Some(log::LevelFilter::Debug)
97                }))
98            );
99        }
100    }
101
102    mod or_default {
103        use crate::LogDestination;
104
105        use super::*;
106
107        #[test]
108        fn no_flags_present_chooses_default() {
109            let args = LogArgs { log: vec![] };
110            let default = vec![LogDestinationConfig {
111                destination: LogDestination::Stderr,
112                level: Some(log::LevelFilter::Info),
113            }];
114            let parsed = args.or_default(LoggingConfig::new(default.clone()));
115            assert_eq!(default, parsed.destinations());
116        }
117
118        #[test]
119        fn none_flag_present() {
120            let args = LogArgs { log: vec![None] };
121            let parsed = args.or_default(LoggingConfig::new(vec![LogDestinationConfig {
122                destination: LogDestination::Stderr,
123                level: Some(log::LevelFilter::Info),
124            }]));
125            assert_eq!(parsed.destinations().len(), 0);
126        }
127
128        #[test]
129        fn one_flag_present() {
130            let destinations = vec![LogDestinationConfig {
131                destination: LogDestination::Stderr,
132                level: Some(log::LevelFilter::Info),
133            }];
134            let args = LogArgs {
135                log: destinations.iter().cloned().map(Some).collect(),
136            };
137            let parsed = args.or_default(LoggingConfig::new(vec![]));
138            assert_eq!(destinations, parsed.destinations());
139        }
140
141        #[test]
142        fn two_flags_present() {
143            let destinations = vec![
144                LogDestinationConfig {
145                    destination: LogDestination::Stderr,
146                    level: Some(log::LevelFilter::Info),
147                },
148                LogDestinationConfig {
149                    destination: LogDestination::File(std::path::PathBuf::from("/tmp/logfile")),
150                    level: Some(log::LevelFilter::Debug),
151                },
152            ];
153            let args = LogArgs {
154                log: destinations.iter().cloned().map(Some).collect(),
155            };
156            let parsed = args.or_default(LoggingConfig::new(vec![]));
157            assert_eq!(destinations, parsed.destinations());
158        }
159
160        #[test]
161        fn two_flags_with_one_none_present() {
162            let first_flag = LogDestinationConfig {
163                destination: LogDestination::Stderr,
164                level: Some(log::LevelFilter::Info),
165            };
166            let destinations = vec![Some(first_flag.clone()), None];
167            let args = LogArgs { log: destinations };
168            let parsed = args.or_default(LoggingConfig::new(vec![]));
169            assert_eq!(vec![first_flag], parsed.destinations());
170        }
171    }
172}