1use std::collections::BTreeMap;
2use std::fs;
3use std::path::Path;
4
5pub fn load_dotenv_variables(project_root_hint: &Path) -> BTreeMap<String, String> {
6 let path = project_root_hint.join(".env");
7 if !path.is_file() {
8 return BTreeMap::new();
9 }
10 let text = match fs::read_to_string(&path) {
11 Ok(text) => text,
12 Err(_) => return BTreeMap::new(),
13 };
14 parse_dotenv_text(&text)
15}
16
17fn parse_dotenv_text(text: &str) -> BTreeMap<String, String> {
18 let mut variables = BTreeMap::new();
19 for line in text.lines() {
20 let trimmed = line.trim();
21 if trimmed.is_empty() || trimmed.starts_with('#') {
22 continue;
23 }
24 let assignment = trimmed.strip_prefix("export ").unwrap_or(trimmed);
25 let Some((key, value)) = assignment.split_once('=') else {
26 continue;
27 };
28 let key = key.trim();
29 if key.is_empty() {
30 continue;
31 }
32 variables.insert(key.to_string(), parse_dotenv_value(value.trim()));
33 }
34 variables
35}
36
37fn parse_dotenv_value(value: &str) -> String {
38 if value.len() >= 2 {
39 let bytes = value.as_bytes();
40 let quote = bytes[0];
41 if (quote == b'"' || quote == b'\'') && bytes[bytes.len() - 1] == quote {
42 return value[1..value.len() - 1].to_string();
43 }
44 }
45 value.to_string()
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51
52 #[test]
53 fn parses_common_dotenv_forms() {
54 let variables = parse_dotenv_text(
55 r#"
56# comment
57export PRAY_ENV=development
58PRAY_PATH="/tmp/project"
59PRAY_FILE_PATH='configs/Prayfile'
60"#,
61 );
62 assert_eq!(
63 variables.get("PRAY_ENV").map(String::as_str),
64 Some("development")
65 );
66 assert_eq!(
67 variables.get("PRAY_PATH").map(String::as_str),
68 Some("/tmp/project")
69 );
70 assert_eq!(
71 variables.get("PRAY_FILE_PATH").map(String::as_str),
72 Some("configs/Prayfile")
73 );
74 }
75}