dotenv_parser/
parser.rs

1use std::collections::BTreeMap;
2
3use pest::iterators::Pair;
4use pest::Parser;
5
6#[derive(Parser)]
7#[grammar = "dotenv.pest"]
8struct DotenvLineParser;
9
10/// Parse the .env file source.
11pub fn parse_dotenv(
12    source: &str,
13) -> Result<BTreeMap<String, String>, Box<dyn std::error::Error + Send + Sync>>
14{
15    let mut map = BTreeMap::new();
16    let pairs = DotenvLineParser::parse(Rule::env, source)?;
17    for pair in pairs {
18        match pair.as_rule() {
19            Rule::kv => {
20                if let Some((key, value)) = parse_kv(pair) {
21                    map.insert(key, value);
22                }
23            }
24            _ => {}
25        }
26    }
27    Ok(map)
28}
29
30/// Parse a key-value pair.
31fn parse_kv(pair: Pair<Rule>) -> Option<(String, String)> {
32    match pair.as_rule() {
33        Rule::kv => {
34            let mut inner_rules = pair.into_inner(); // key ~ "=" ~ value
35            let name: &str = inner_rules.next().unwrap().as_str();
36            parse_value(inner_rules.next().unwrap()).map(|v| (name.into(), v))
37        }
38        _ => None,
39    }
40}
41
42/// Parse a value, which might be a string or a naked variable.
43fn parse_value(pair: Pair<Rule>) -> Option<String> {
44    match pair.as_rule() {
45        Rule::value => {
46            let inner = pair.clone().into_inner().next();
47            // If there are no inner pairs, the current value is a naked
48            // variable, otherwise it's a string and we need to extract the
49            // inner_sq or inner_dq pair.
50            match inner {
51                None => Some(pair.as_str().into()),
52                Some(inner_pair) => match inner_pair.into_inner().next() {
53                    None => None,
54                    Some(inner_string) => Some(inner_string.as_str().into()),
55                },
56            }
57        }
58        _ => None,
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::parse_dotenv;
65    use std::collections::BTreeMap;
66
67    #[test]
68    fn empty_file() {
69        assert_eq!(parse_dotenv("").unwrap(), BTreeMap::new());
70    }
71
72    #[test]
73    fn one_kv() {
74        let bm = vec![("key", "value")]
75            .into_iter()
76            .map(|(a, b)| (a.into(), b.into()))
77            .collect();
78        assert_eq!(parse_dotenv("key = value").unwrap(), bm);
79    }
80
81    #[test]
82    fn one_line() {
83        let bm = vec![("key", "value")]
84            .into_iter()
85            .map(|(a, b)| (a.into(), b.into()))
86            .collect();
87        assert_eq!(parse_dotenv("key = value\n").unwrap(), bm);
88    }
89
90    #[test]
91    fn two_lines() {
92        let bm = vec![("key", "value"), ("key2", "value2")]
93            .into_iter()
94            .map(|(a, b)| (a.into(), b.into()))
95            .collect();
96        assert_eq!(parse_dotenv("key = value\nkey2 = value2").unwrap(), bm);
97    }
98
99    #[test]
100    fn non_alphanumeric_chars() {
101        let bm = vec![("key", "https://1.3.2.3:234/a?b=c")]
102            .into_iter()
103            .map(|(a, b)| (a.into(), b.into()))
104            .collect();
105        assert_eq!(
106            parse_dotenv("key=https://1.3.2.3:234/a?b=c\n").unwrap(),
107            bm
108        );
109    }
110
111    #[test]
112    fn export() {
113        let bm = vec![("key", "value"), ("key2", "value2")]
114            .into_iter()
115            .map(|(a, b)| (a.into(), b.into()))
116            .collect();
117        assert_eq!(
118            parse_dotenv("key = value\nexport key2 = value2").unwrap(),
119            bm
120        );
121    }
122
123    #[test]
124    fn string_single_quotes() {
125        let bm = vec![("key", "value"), ("key2", "val ue2")]
126            .into_iter()
127            .map(|(a, b)| (a.into(), b.into()))
128            .collect();
129        assert_eq!(parse_dotenv("key = value\nkey2 = 'val ue2'").unwrap(), bm);
130    }
131
132    #[test]
133    fn string_double_quotes() {
134        let bm = vec![("key", "value"), ("key2", "val ue2")]
135            .into_iter()
136            .map(|(a, b)| (a.into(), b.into()))
137            .collect();
138        assert_eq!(
139            parse_dotenv("key = value\nkey2 = \"val ue2\"").unwrap(),
140            bm
141        );
142    }
143
144    #[test]
145    fn empty_value_single_quotes() {
146        let bm = vec![("key", "value"), ("key2", "")]
147            .into_iter()
148            .map(|(a, b)| (a.into(), b.into()))
149            .collect();
150        assert_eq!(parse_dotenv("key = value\nkey2 = ''").unwrap(), bm);
151    }
152
153    #[test]
154    fn empty_value_double_quotes() {
155        let bm = vec![("key", "value"), ("key2", "")]
156            .into_iter()
157            .map(|(a, b)| (a.into(), b.into()))
158            .collect();
159        assert_eq!(parse_dotenv("key = value\nkey2 = \"\"").unwrap(), bm);
160    }
161
162    #[test]
163    fn comments() {
164        let source = r#"
165            # one here
166            ENV_FOR_HYDRO=production # another one here
167        "#;
168        let bm = vec![("ENV_FOR_HYDRO", "production")]
169            .into_iter()
170            .map(|(a, b)| (a.into(), b.into()))
171            .collect();
172        assert_eq!(parse_dotenv(source).unwrap(), bm);
173    }
174
175    #[test]
176    fn complete_dotenv() {
177        let source = r#"
178            # main comment
179
180            ENV_FOR_HYDRO='testing 2' # another one here
181            export USER_ID=5gpPN5rcv5G41U_S
182            API_TOKEN=30af563ccc668bc8ced9e24e  # relax! these values are fake
183            APP_SITE_URL=https://my.example.com
184        "#;
185        let bm = vec![
186            ("ENV_FOR_HYDRO", "testing 2"),
187            ("USER_ID", "5gpPN5rcv5G41U_S"),
188            ("API_TOKEN", "30af563ccc668bc8ced9e24e"),
189            ("APP_SITE_URL", "https://my.example.com"),
190        ]
191        .into_iter()
192        .map(|(a, b)| (a.into(), b.into()))
193        .collect();
194        assert_eq!(parse_dotenv(source).unwrap(), bm);
195    }
196}