env_filter/
parser.rs

1use log::LevelFilter;
2
3use crate::Directive;
4use crate::FilterOp;
5
6/// Parse a logging specification string (e.g: "crate1,crate2::mod3,crate3::x=error/foo")
7/// and return a vector with log directives.
8pub(crate) fn parse_spec(spec: &str) -> (Vec<Directive>, Option<FilterOp>) {
9    let mut dirs = Vec::new();
10
11    let mut parts = spec.split('/');
12    let mods = parts.next();
13    let filter = parts.next();
14    if parts.next().is_some() {
15        eprintln!(
16            "warning: invalid logging spec '{}', \
17             ignoring it (too many '/'s)",
18            spec
19        );
20        return (dirs, None);
21    }
22    if let Some(m) = mods {
23        for s in m.split(',').map(|ss| ss.trim()) {
24            if s.is_empty() {
25                continue;
26            }
27            let mut parts = s.split('=');
28            let (log_level, name) =
29                match (parts.next(), parts.next().map(|s| s.trim()), parts.next()) {
30                    (Some(part0), None, None) => {
31                        // if the single argument is a log-level string or number,
32                        // treat that as a global fallback
33                        match part0.parse() {
34                            Ok(num) => (num, None),
35                            Err(_) => (LevelFilter::max(), Some(part0)),
36                        }
37                    }
38                    (Some(part0), Some(""), None) => (LevelFilter::max(), Some(part0)),
39                    (Some(part0), Some(part1), None) => match part1.parse() {
40                        Ok(num) => (num, Some(part0)),
41                        _ => {
42                            eprintln!(
43                                "warning: invalid logging spec '{}', \
44                                 ignoring it",
45                                part1
46                            );
47                            continue;
48                        }
49                    },
50                    _ => {
51                        eprintln!(
52                            "warning: invalid logging spec '{}', \
53                             ignoring it",
54                            s
55                        );
56                        continue;
57                    }
58                };
59            dirs.push(Directive {
60                name: name.map(|s| s.to_string()),
61                level: log_level,
62            });
63        }
64    }
65
66    let filter = filter.and_then(|filter| match FilterOp::new(filter) {
67        Ok(re) => Some(re),
68        Err(e) => {
69            eprintln!("warning: invalid regex filter - {}", e);
70            None
71        }
72    });
73
74    (dirs, filter)
75}
76
77#[cfg(test)]
78mod tests {
79    use log::LevelFilter;
80
81    use super::parse_spec;
82
83    #[test]
84    fn parse_spec_valid() {
85        let (dirs, filter) = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug");
86        assert_eq!(dirs.len(), 3);
87        assert_eq!(dirs[0].name, Some("crate1::mod1".to_string()));
88        assert_eq!(dirs[0].level, LevelFilter::Error);
89
90        assert_eq!(dirs[1].name, Some("crate1::mod2".to_string()));
91        assert_eq!(dirs[1].level, LevelFilter::max());
92
93        assert_eq!(dirs[2].name, Some("crate2".to_string()));
94        assert_eq!(dirs[2].level, LevelFilter::Debug);
95        assert!(filter.is_none());
96    }
97
98    #[test]
99    fn parse_spec_invalid_crate() {
100        // test parse_spec with multiple = in specification
101        let (dirs, filter) = parse_spec("crate1::mod1=warn=info,crate2=debug");
102        assert_eq!(dirs.len(), 1);
103        assert_eq!(dirs[0].name, Some("crate2".to_string()));
104        assert_eq!(dirs[0].level, LevelFilter::Debug);
105        assert!(filter.is_none());
106    }
107
108    #[test]
109    fn parse_spec_invalid_level() {
110        // test parse_spec with 'noNumber' as log level
111        let (dirs, filter) = parse_spec("crate1::mod1=noNumber,crate2=debug");
112        assert_eq!(dirs.len(), 1);
113        assert_eq!(dirs[0].name, Some("crate2".to_string()));
114        assert_eq!(dirs[0].level, LevelFilter::Debug);
115        assert!(filter.is_none());
116    }
117
118    #[test]
119    fn parse_spec_string_level() {
120        // test parse_spec with 'warn' as log level
121        let (dirs, filter) = parse_spec("crate1::mod1=wrong,crate2=warn");
122        assert_eq!(dirs.len(), 1);
123        assert_eq!(dirs[0].name, Some("crate2".to_string()));
124        assert_eq!(dirs[0].level, LevelFilter::Warn);
125        assert!(filter.is_none());
126    }
127
128    #[test]
129    fn parse_spec_empty_level() {
130        // test parse_spec with '' as log level
131        let (dirs, filter) = parse_spec("crate1::mod1=wrong,crate2=");
132        assert_eq!(dirs.len(), 1);
133        assert_eq!(dirs[0].name, Some("crate2".to_string()));
134        assert_eq!(dirs[0].level, LevelFilter::max());
135        assert!(filter.is_none());
136    }
137
138    #[test]
139    fn parse_spec_empty_level_isolated() {
140        // test parse_spec with "" as log level (and the entire spec str)
141        let (dirs, filter) = parse_spec(""); // should be ignored
142        assert_eq!(dirs.len(), 0);
143        assert!(filter.is_none());
144    }
145
146    #[test]
147    fn parse_spec_blank_level_isolated() {
148        // test parse_spec with a white-space-only string specified as the log
149        // level (and the entire spec str)
150        let (dirs, filter) = parse_spec("     "); // should be ignored
151        assert_eq!(dirs.len(), 0);
152        assert!(filter.is_none());
153    }
154
155    #[test]
156    fn parse_spec_blank_level_isolated_comma_only() {
157        // The spec should contain zero or more comma-separated string slices,
158        // so a comma-only string should be interpreted as two empty strings
159        // (which should both be treated as invalid, so ignored).
160        let (dirs, filter) = parse_spec(","); // should be ignored
161        assert_eq!(dirs.len(), 0);
162        assert!(filter.is_none());
163    }
164
165    #[test]
166    fn parse_spec_blank_level_isolated_comma_blank() {
167        // The spec should contain zero or more comma-separated string slices,
168        // so this bogus spec should be interpreted as containing one empty
169        // string and one blank string. Both should both be treated as
170        // invalid, so ignored.
171        let (dirs, filter) = parse_spec(",     "); // should be ignored
172        assert_eq!(dirs.len(), 0);
173        assert!(filter.is_none());
174    }
175
176    #[test]
177    fn parse_spec_blank_level_isolated_blank_comma() {
178        // The spec should contain zero or more comma-separated string slices,
179        // so this bogus spec should be interpreted as containing one blank
180        // string and one empty string. Both should both be treated as
181        // invalid, so ignored.
182        let (dirs, filter) = parse_spec("     ,"); // should be ignored
183        assert_eq!(dirs.len(), 0);
184        assert!(filter.is_none());
185    }
186
187    #[test]
188    fn parse_spec_global() {
189        // test parse_spec with no crate
190        let (dirs, filter) = parse_spec("warn,crate2=debug");
191        assert_eq!(dirs.len(), 2);
192        assert_eq!(dirs[0].name, None);
193        assert_eq!(dirs[0].level, LevelFilter::Warn);
194        assert_eq!(dirs[1].name, Some("crate2".to_string()));
195        assert_eq!(dirs[1].level, LevelFilter::Debug);
196        assert!(filter.is_none());
197    }
198
199    #[test]
200    fn parse_spec_global_bare_warn_lc() {
201        // test parse_spec with no crate, in isolation, all lowercase
202        let (dirs, filter) = parse_spec("warn");
203        assert_eq!(dirs.len(), 1);
204        assert_eq!(dirs[0].name, None);
205        assert_eq!(dirs[0].level, LevelFilter::Warn);
206        assert!(filter.is_none());
207    }
208
209    #[test]
210    fn parse_spec_global_bare_warn_uc() {
211        // test parse_spec with no crate, in isolation, all uppercase
212        let (dirs, filter) = parse_spec("WARN");
213        assert_eq!(dirs.len(), 1);
214        assert_eq!(dirs[0].name, None);
215        assert_eq!(dirs[0].level, LevelFilter::Warn);
216        assert!(filter.is_none());
217    }
218
219    #[test]
220    fn parse_spec_global_bare_warn_mixed() {
221        // test parse_spec with no crate, in isolation, mixed case
222        let (dirs, filter) = parse_spec("wArN");
223        assert_eq!(dirs.len(), 1);
224        assert_eq!(dirs[0].name, None);
225        assert_eq!(dirs[0].level, LevelFilter::Warn);
226        assert!(filter.is_none());
227    }
228
229    #[test]
230    fn parse_spec_valid_filter() {
231        let (dirs, filter) = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug/abc");
232        assert_eq!(dirs.len(), 3);
233        assert_eq!(dirs[0].name, Some("crate1::mod1".to_string()));
234        assert_eq!(dirs[0].level, LevelFilter::Error);
235
236        assert_eq!(dirs[1].name, Some("crate1::mod2".to_string()));
237        assert_eq!(dirs[1].level, LevelFilter::max());
238
239        assert_eq!(dirs[2].name, Some("crate2".to_string()));
240        assert_eq!(dirs[2].level, LevelFilter::Debug);
241        assert!(filter.is_some() && filter.unwrap().to_string() == "abc");
242    }
243
244    #[test]
245    fn parse_spec_invalid_crate_filter() {
246        let (dirs, filter) = parse_spec("crate1::mod1=error=warn,crate2=debug/a.c");
247        assert_eq!(dirs.len(), 1);
248        assert_eq!(dirs[0].name, Some("crate2".to_string()));
249        assert_eq!(dirs[0].level, LevelFilter::Debug);
250        assert!(filter.is_some() && filter.unwrap().to_string() == "a.c");
251    }
252
253    #[test]
254    fn parse_spec_empty_with_filter() {
255        let (dirs, filter) = parse_spec("crate1/a*c");
256        assert_eq!(dirs.len(), 1);
257        assert_eq!(dirs[0].name, Some("crate1".to_string()));
258        assert_eq!(dirs[0].level, LevelFilter::max());
259        assert!(filter.is_some() && filter.unwrap().to_string() == "a*c");
260    }
261}