1use std::collections::HashMap;
8
9pub fn parse_dotenv(contents: &str) -> HashMap<String, String> {
14 let mut result = HashMap::new();
15 for line in contents.lines() {
16 let trimmed = line.trim();
17
18 if trimmed.is_empty() || trimmed.starts_with('#') {
20 continue;
21 }
22
23 let stripped = trimmed.strip_prefix("export ").unwrap_or(trimmed);
25
26 let Some((key, raw_value)) = stripped.split_once('=') else {
27 continue;
28 };
29
30 let key = key.trim();
31 if key.is_empty() {
32 continue;
33 }
34
35 let value = unquote(raw_value.trim());
36 result.insert(key.to_string(), value);
37 }
38 result
39}
40
41fn unquote(s: &str) -> String {
43 if s.len() >= 2
44 && ((s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')))
45 {
46 return s[1..s.len() - 1].to_string();
47 }
48 s.to_string()
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 #[test]
56 fn basic_key_value() {
57 let env = parse_dotenv("API_KEY=sk-abc123");
58 assert_eq!(env.get("API_KEY").unwrap(), "sk-abc123");
59 }
60
61 #[test]
62 fn multiple_pairs() {
63 let env = parse_dotenv("A=1\nB=2\nC=3");
64 assert_eq!(env.len(), 3);
65 assert_eq!(env["A"], "1");
66 assert_eq!(env["B"], "2");
67 assert_eq!(env["C"], "3");
68 }
69
70 #[test]
71 fn comments_and_blanks_skipped() {
72 let input = "# This is a comment\n\nKEY=value\n # Another comment\n \n";
73 let env = parse_dotenv(input);
74 assert_eq!(env.len(), 1);
75 assert_eq!(env["KEY"], "value");
76 }
77
78 #[test]
79 fn double_quoted_value() {
80 let env = parse_dotenv(r#"SECRET="hello world""#);
81 assert_eq!(env["SECRET"], "hello world");
82 }
83
84 #[test]
85 fn single_quoted_value() {
86 let env = parse_dotenv("SECRET='hello world'");
87 assert_eq!(env["SECRET"], "hello world");
88 }
89
90 #[test]
91 fn export_prefix_stripped() {
92 let env = parse_dotenv("export API_KEY=sk-abc123");
93 assert_eq!(env["API_KEY"], "sk-abc123");
94 }
95
96 #[test]
97 fn export_with_quotes() {
98 let env = parse_dotenv(r#"export SECRET="my value""#);
99 assert_eq!(env["SECRET"], "my value");
100 }
101
102 #[test]
103 fn value_with_equals_sign() {
104 let env = parse_dotenv("URL=https://example.com?foo=bar");
105 assert_eq!(env["URL"], "https://example.com?foo=bar");
106 }
107
108 #[test]
109 fn empty_value() {
110 let env = parse_dotenv("EMPTY=");
111 assert_eq!(env["EMPTY"], "");
112 }
113
114 #[test]
115 fn whitespace_around_key_and_value() {
116 let env = parse_dotenv(" KEY = value ");
117 assert_eq!(env["KEY"], "value");
118 }
119
120 #[test]
121 fn line_without_equals_skipped() {
122 let env = parse_dotenv("NOEQUALS\nKEY=value");
123 assert_eq!(env.len(), 1);
124 assert_eq!(env["KEY"], "value");
125 }
126
127 #[test]
128 fn empty_key_skipped() {
129 let env = parse_dotenv("=value");
130 assert!(env.is_empty());
131 }
132
133 #[test]
134 fn empty_input() {
135 let env = parse_dotenv("");
136 assert!(env.is_empty());
137 }
138
139 #[test]
140 fn quoted_empty_value() {
141 let env = parse_dotenv(r#"KEY="""#);
142 assert_eq!(env["KEY"], "");
143 }
144
145 #[test]
146 fn mixed_content() {
147 let input = "\
148# Database config
149DB_HOST=localhost
150DB_PORT=5432
151export DB_PASS=\"s3cret\"
152
153# API keys
154API_KEY='sk-test-123'
155";
156 let env = parse_dotenv(input);
157 assert_eq!(env.len(), 4);
158 assert_eq!(env["DB_HOST"], "localhost");
159 assert_eq!(env["DB_PORT"], "5432");
160 assert_eq!(env["DB_PASS"], "s3cret");
161 assert_eq!(env["API_KEY"], "sk-test-123");
162 }
163}