Skip to main content

dotenv/
dotenv.rs

1use crate::parse::*;
2use std::collections::HashMap;
3
4/// Parsed dotenv content
5pub struct Dotenv {
6    buf: String,
7}
8
9fn normalize_input(buf: String) -> String {
10    // Strip UTF-8 BOM (\u{FEFF}) if present — the parser does not expect it
11    let buf = if let Some(stripped) = buf.strip_prefix('\u{FEFF}') {
12        stripped.to_string()
13    } else {
14        buf
15    };
16
17    // Normalise line endings per spec: \r\n → \n, then standalone \r → \n
18    buf.replace("\r\n", "\n").replace('\r', "\n")
19}
20
21impl Dotenv {
22    pub(crate) fn new(buf: String) -> Self {
23        Self {
24            buf: normalize_input(buf),
25        }
26    }
27
28    /// Return an iterator over the dotenv entries.
29    pub fn iter(&self) -> Iter<'_> {
30        Iter::new(&self.buf)
31    }
32
33    /// Load the dotenv into the current process's environment variables.
34    ///
35    /// Existing variables are **not** overwritten.
36    pub fn load(self) {
37        self.set_vars(false)
38    }
39
40    /// Load the dotenv into the current process's environment variables,
41    /// **overwriting** any existing values.
42    pub fn load_override(self) {
43        self.set_vars(true)
44    }
45
46    fn set_vars(self, override_env: bool) {
47        for (key, value) in self.iter() {
48            if override_env || std::env::var(key).is_err() {
49                std::env::set_var(key, value);
50            }
51        }
52    }
53}
54
55/// An iterator over the entries in a dotenv file.
56///
57/// Yields `(&str, String)` pairs. Variable substitutions are resolved
58/// lazily against both the process environment and previously-yielded
59/// entries within the same file.
60pub struct Iter<'a> {
61    resolved: HashMap<&'a str, String>,
62    input: &'a str,
63}
64
65impl<'a> Iter<'a> {
66    pub(crate) fn new(input: &'a str) -> Self {
67        Self {
68            resolved: HashMap::new(),
69            input,
70        }
71    }
72
73    /// Resolve a variable name to a non-empty value.
74    ///
75    /// Checks the process environment first, then previously-resolved
76    /// entries from this file.
77    fn resolve_var(&self, name: &'a str) -> Option<String> {
78        std::env::var(name)
79            .ok()
80            .or_else(|| self.resolved.get(name).cloned())
81            .filter(|it| !it.is_empty())
82    }
83
84    fn resolve(&self, value: Value<'a>) -> String {
85        match value {
86            Value::Lit(text) => text.to_string(),
87            Value::Sub(name, fallback) => self
88                .resolve_var(name)
89                .or_else(|| fallback.map(|it| self.resolve(*it)))
90                .unwrap_or_default(),
91            Value::List(list) => list.into_iter().map(|it| self.resolve(it)).collect(),
92        }
93    }
94}
95
96impl<'a> Iterator for Iter<'a> {
97    type Item = (&'a str, String);
98
99    fn next(&mut self) -> Option<Self::Item> {
100        while !self.input.is_empty() {
101            match parse(&mut self.input) {
102                Ok(Some((key, value))) => {
103                    let resolved = self.resolve(value);
104                    self.resolved.insert(key, resolved.clone());
105                    return Some((key, resolved));
106                }
107                Ok(None) => {
108                    // comment or blank line, continue
109                }
110                Err(_) => {
111                    // parse error
112                    break;
113                }
114            }
115        }
116
117        None
118    }
119}