1use std::collections::BTreeMap;
6use std::ffi::{OsStr, OsString};
7use std::path::{Path, PathBuf};
8use std::sync::OnceLock;
9
10use anyhow::{Context, Result};
11use regex::Regex;
12
13pub fn parse(text: &str) -> Vec<(String, String)> {
18 static LINE: OnceLock<Regex> = OnceLock::new();
19 let line = LINE.get_or_init(|| {
21 Regex::new(
22 r#"(?m)(?:^|^)\s*(?:export\s+)?((?-u:[\w.-])+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?$"#,
23 )
24 .unwrap()
25 });
26 let text = text.replace("\r\n", "\n").replace('\r', "\n");
27 let mut entries: Vec<(String, String)> = Vec::new();
28 for captures in line.captures_iter(&text) {
29 let key = captures[1].to_owned();
30 let raw = captures.get(2).map_or("", |value| value.as_str()).trim();
31 let quote = raw.chars().next();
32 let mut value = match quote {
33 Some(quote @ ('\'' | '"' | '`')) if raw.len() >= 2 && raw.ends_with(quote) => {
34 raw[1..raw.len() - 1].to_owned()
35 }
36 _ => raw.to_owned(),
37 };
38 if quote == Some('"') {
39 value = value.replace("\\n", "\n").replace("\\r", "\r");
40 }
41 match entries.iter_mut().find(|(name, _)| *name == key) {
42 Some(entry) => entry.1 = value,
43 None => entries.push((key, value)),
44 }
45 }
46 entries
47}
48
49pub fn load(
56 paths: &[impl AsRef<Path>],
57 env: &mut BTreeMap<OsString, OsString>,
58) -> Result<Vec<PathBuf>> {
59 let mut parsed: Vec<(String, String)> = Vec::new();
60 let mut read = Vec::new();
61 for path in paths {
62 let path = path.as_ref();
63 let text = match std::fs::read(path) {
64 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
65 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
66 Err(error) => {
67 return Err(error).with_context(|| format!("cannot read {}", path.display()));
68 }
69 };
70 read.push(path.to_path_buf());
71 for (key, value) in parse(&text) {
72 if !parsed.iter().any(|(name, _)| *name == key) {
73 parsed.push((key, value));
74 }
75 }
76 }
77 for (key, value) in &parsed {
78 let name = crate::environment_key(env, key.into());
79 env.entry(name).or_insert_with(|| value.into());
80 }
81 let mut running: BTreeMap<String, String> = BTreeMap::new();
84 let mut expanded = Vec::new();
85 for (key, value) in &parsed {
86 let name = crate::environment_key(env, key.into());
87 let current = env
88 .get(&name)
89 .map(|value| value.to_string_lossy().into_owned());
90 let value = match current {
91 Some(current) if !current.is_empty() && current != *value => current,
92 _ => expand(value, env, &running),
93 };
94 let value = value.replace("\\$", "$");
95 running.insert(key.clone(), value.clone());
96 expanded.push((name, value));
97 }
98 for (name, value) in expanded {
99 env.insert(name, value.into());
100 }
101 Ok(read)
102}
103
104pub fn read_alone(path: &Path) -> Result<BTreeMap<OsString, OsString>> {
107 let mut env = BTreeMap::new();
108 load(&[path], &mut env)?;
109 Ok(env)
110}
111
112fn expand(
119 value: &str,
120 env: &BTreeMap<OsString, OsString>,
121 running: &BTreeMap<String, String>,
122) -> String {
123 let lookup = |key: &str| -> Option<String> {
124 env.get(OsStr::new(key))
125 .map(|value| value.to_string_lossy().into_owned())
126 .or_else(|| running.get(key).cloned())
127 .filter(|value| !value.is_empty())
128 };
129 let mut result = value.to_owned();
130 let mut seen: Vec<String> = Vec::new();
131 while let Some((start, end, expression)) = reference(&result) {
132 seen.push(result.clone());
133 let (key, operator, rest) = match ["-", ":-", "+", ":+"]
134 .iter()
135 .filter_map(|operator| expression.find(operator).map(|index| (index, *operator)))
136 .min_by_key(|(index, operator)| (*index, std::cmp::Reverse(operator.len())))
138 {
139 Some((index, operator)) => (
140 &expression[..index],
141 Some(operator),
142 expression[index..]
143 .split(operator)
144 .skip(1)
145 .collect::<Vec<_>>()
146 .join(operator),
147 ),
148 None => (expression.as_str(), None, String::new()),
149 };
150 let replacement = match operator {
151 Some("+" | ":+") => {
152 if lookup(key).is_some() {
153 rest
154 } else {
155 String::new()
156 }
157 }
158 _ => match lookup(key) {
159 Some(found) if seen.contains(&found) => rest,
160 Some(found) => found,
161 None => rest,
162 },
163 };
164 result.replace_range(start..end, &replacement);
165 if running.get(key) == Some(&result) {
166 break;
167 }
168 }
169 result
170}
171
172fn reference(text: &str) -> Option<(usize, usize, String)> {
175 let bytes = text.as_bytes();
176 let mut index = 0;
177 while let Some(offset) = text[index..].find('$') {
178 let start = index + offset;
179 index = start + 1;
180 if start > 0 && bytes[start - 1] == b'\\' {
181 continue;
182 }
183 let rest = &text[start + 1..];
184 if let Some(inner) = rest.strip_prefix('{') {
185 if let Some(close) = inner.find(['{', '}'])
186 && inner.as_bytes()[close] == b'}'
187 && close > 0
188 {
189 return Some((start, start + 2 + close + 1, inner[..close].to_owned()));
190 }
191 continue;
192 }
193 let name: String = rest
194 .chars()
195 .enumerate()
196 .take_while(|(position, character)| {
197 character.is_ascii_alphabetic()
198 || *character == '_'
199 || (*position > 0 && character.is_ascii_digit())
200 })
201 .map(|(_, character)| character)
202 .collect();
203 if !name.is_empty() {
204 return Some((start, start + 1 + name.len(), name));
205 }
206 }
207 None
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 fn parsed(text: &str) -> Vec<(String, String)> {
215 parse(text)
216 }
217
218 fn pairs(entries: &[(&str, &str)]) -> Vec<(String, String)> {
219 entries
220 .iter()
221 .map(|(key, value)| ((*key).to_owned(), (*value).to_owned()))
222 .collect()
223 }
224
225 #[test]
226 fn parses_as_npm_dotenv_does() {
227 assert_eq!(
228 parsed(
229 "# comment\nexport A=1\nB = two words # note\nC='single $X'\nD=\"line\\nbreak\"\nE=`tick`\nF=\n G=x\nnot a line\nH: yaml\nB=again\n"
230 ),
231 pairs(&[
232 ("A", "1"),
233 ("B", "again"),
234 ("C", "single $X"),
235 ("D", "line\nbreak"),
236 ("E", "tick"),
237 ("F", ""),
238 ("G", "x"),
239 ("H", "yaml"),
240 ])
241 );
242 assert_eq!(
244 parsed("A=\"one\ntwo\"\nB=\"say \\\"hi\\\"\"\nC=a#b\n"),
245 pairs(&[("A", "one\ntwo"), ("B", "say \\\"hi\\\""), ("C", "a")])
246 );
247 }
248
249 fn env(entries: &[(&str, &str)]) -> BTreeMap<OsString, OsString> {
250 entries
251 .iter()
252 .map(|(key, value)| ((*key).into(), (*value).into()))
253 .collect()
254 }
255
256 #[test]
257 fn expands_once_across_every_file() {
258 let directory = tempfile::tempdir().unwrap();
259 let first = directory.path().join(".env.build");
260 let second = directory.path().join(".env");
261 std::fs::write(&first, "URL=${HOST}:${PORT}\nNAME=first\n").unwrap();
262 std::fs::write(
263 &second,
264 "HOST=localhost\nPORT=${DEFAULT_PORT:-80}\nNAME=second\nSHELL_SET=file\nEMPTY=filled\nLITERAL=\\$HOME\nALT=${HOST:+yes}\n",
265 )
266 .unwrap();
267 let mut variables = env(&[("SHELL_SET", "shell"), ("EMPTY", "")]);
268 load(&[&first, &second], &mut variables).unwrap();
269 let get = |name: &str| variables[OsStr::new(name)].to_str().unwrap().to_owned();
270 assert_eq!(get("URL"), "localhost:80");
271 assert_eq!(get("NAME"), "first");
272 assert_eq!(get("SHELL_SET"), "shell");
273 assert_eq!(get("EMPTY"), "filled");
275 assert_eq!(get("LITERAL"), "$HOME");
276 assert_eq!(get("ALT"), "yes");
277 }
278
279 #[test]
280 fn a_reference_to_itself_ends() {
281 let mut variables = env(&[]);
282 let directory = tempfile::tempdir().unwrap();
283 let file = directory.path().join(".env");
284 std::fs::write(&file, "A=${A}\nB=x${B:-fallback}\n").unwrap();
285 load(&[&file], &mut variables).unwrap();
286 assert_eq!(variables[OsStr::new("A")], "");
287 assert_eq!(variables[OsStr::new("B")], "xfallback");
288 }
289}