1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3use std::{collections::HashSet, env, fmt, fs, io};
43
44#[derive(Debug)]
46pub enum Error {
47 Io(io::Error),
49 Parse(ParseError),
51}
52
53impl fmt::Display for Error {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 match self {
56 Error::Io(e) => write!(f, "dotenv I/O error: {e}"),
57 Error::Parse(e) => write!(f, "dotenv parse error at line {}: {}", e.line, e.kind),
58 }
59 }
60}
61
62impl std::error::Error for Error {
63 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
64 match self {
65 Error::Io(e) => Some(e),
66 Error::Parse(_) => None,
67 }
68 }
69}
70
71impl From<io::Error> for Error {
72 fn from(e: io::Error) -> Self {
73 Error::Io(e)
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct ParseError {
80 pub line: usize,
82 pub kind: ParseErrorKind,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum ParseErrorKind {
89 MissingEquals,
91 UnmatchedQuote,
93 EmptyKey,
95 InvalidKey,
98 TrailingContent,
100}
101
102impl fmt::Display for ParseErrorKind {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 match self {
105 ParseErrorKind::MissingEquals => f.write_str("missing equals sign"),
106 ParseErrorKind::UnmatchedQuote => f.write_str("unmatched quote"),
107 ParseErrorKind::EmptyKey => f.write_str("empty key"),
108 ParseErrorKind::InvalidKey => f.write_str("invalid key character"),
109 ParseErrorKind::TrailingContent => f.write_str("trailing content after closing quote"),
110 }
111 }
112}
113
114pub fn load() -> Result<(), Error> {
138 let mut path = env::current_dir()?;
139 path.push(".env");
140 let content = fs::read_to_string(&path)?;
141 let pairs = parse(&content)?;
142
143 let existing: HashSet<String> = env::vars().map(|(k, _)| k).collect();
144
145 let mut seen = HashSet::new();
146 for (key, value) in &pairs {
147 if seen.insert(key.clone()) && !existing.contains(key.as_str()) {
148 unsafe { env::set_var(key, value) };
150 }
151 }
152 Ok(())
153}
154
155fn parse(input: &str) -> Result<Vec<(String, String)>, Error> {
157 let mut pairs = Vec::new();
158
159 for (line_idx, raw_line) in input.lines().enumerate() {
160 let line = raw_line.trim_start();
161
162 if line.is_empty() || line.starts_with('#') {
163 continue;
164 }
165
166 let eq_pos = line.find('=').ok_or_else(|| {
167 Error::Parse(ParseError {
168 line: line_idx + 1,
169 kind: ParseErrorKind::MissingEquals,
170 })
171 })?;
172
173 let key = line[..eq_pos].trim_end();
174 let value_str = &line[eq_pos + 1..];
175
176 if key.is_empty() {
177 return Err(Error::Parse(ParseError {
178 line: line_idx + 1,
179 kind: ParseErrorKind::EmptyKey,
180 }));
181 }
182
183 if !key
184 .chars()
185 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-')
186 {
187 return Err(Error::Parse(ParseError {
188 line: line_idx + 1,
189 kind: ParseErrorKind::InvalidKey,
190 }));
191 }
192
193 let value = parse_value(value_str, line_idx + 1)?;
194 pairs.push((key.to_string(), value));
195 }
196
197 Ok(pairs)
198}
199
200fn parse_value(s: &str, line: usize) -> Result<String, Error> {
202 let trimmed = s.trim();
203
204 if trimmed.is_empty() {
205 return Ok(String::new());
206 }
207
208 match trimmed.as_bytes()[0] {
209 b'"' => {
210 let rest = &trimmed[1..];
211 let close = rest.find('"').ok_or_else(|| {
212 Error::Parse(ParseError {
213 line,
214 kind: ParseErrorKind::UnmatchedQuote,
215 })
216 })?;
217 let after = rest[close + 1..].trim();
218 if !after.is_empty() && !after.starts_with('#') {
219 return Err(Error::Parse(ParseError {
220 line,
221 kind: ParseErrorKind::TrailingContent,
222 }));
223 }
224 Ok(rest[..close].to_string())
225 }
226 b'\'' => {
227 let rest = &trimmed[1..];
228 let close = rest.find('\'').ok_or_else(|| {
229 Error::Parse(ParseError {
230 line,
231 kind: ParseErrorKind::UnmatchedQuote,
232 })
233 })?;
234 let after = rest[close + 1..].trim();
235 if !after.is_empty() && !after.starts_with('#') {
236 return Err(Error::Parse(ParseError {
237 line,
238 kind: ParseErrorKind::TrailingContent,
239 }));
240 }
241 Ok(rest[..close].to_string())
242 }
243 _ => {
244 let comment_start = s
245 .as_bytes()
246 .windows(2)
247 .position(|w| w[0].is_ascii_whitespace() && w[1] == b'#')
248 .map(|i| i + 1);
249 let val = match comment_start {
250 Some(pos) => &s[..pos],
251 None => s,
252 };
253 Ok(val.trim().to_string())
254 }
255 }
256}
257
258#[cfg(test)]
263mod tests {
264 use super::*;
265
266 fn parse_ok(input: &str) -> Vec<(String, String)> {
267 parse(input).unwrap()
268 }
269
270 fn parse_kind(input: &str) -> ParseErrorKind {
271 match parse(input).unwrap_err() {
272 Error::Parse(e) => e.kind,
273 _ => panic!("expected Parse error"),
274 }
275 }
276
277 fn parse_line(input: &str) -> usize {
278 match parse(input).unwrap_err() {
279 Error::Parse(e) => e.line,
280 _ => panic!("expected Parse error"),
281 }
282 }
283
284 unsafe fn set_env(k: &str, v: &str) {
286 unsafe { env::set_var(k, v) };
287 }
288
289 unsafe fn remove_env(k: &str) {
290 unsafe { env::remove_var(k) };
291 }
292
293 #[test]
296 fn simple_key_value() {
297 assert_eq!(parse_ok("K=v"), vec![("K".into(), "v".into())]);
298 }
299
300 #[test]
301 fn multiple_pairs() {
302 let pairs = parse_ok("A=1\nB=2\nC=3");
303 assert_eq!(
304 pairs,
305 vec![
306 ("A".into(), "1".into()),
307 ("B".into(), "2".into()),
308 ("C".into(), "3".into()),
309 ]
310 );
311 }
312
313 #[test]
314 fn value_with_equals() {
315 assert_eq!(parse_ok("K=a=b=c"), vec![("K".into(), "a=b=c".into())]);
316 }
317
318 #[test]
319 fn key_with_underscore() {
320 assert_eq!(parse_ok("MY_KEY=val"), vec![("MY_KEY".into(), "val".into())]);
321 }
322
323 #[test]
324 fn key_with_dot() {
325 assert_eq!(parse_ok("my.key=val"), vec![("my.key".into(), "val".into())]);
326 }
327
328 #[test]
329 fn key_with_hyphen() {
330 assert_eq!(parse_ok("my-key=val"), vec![("my-key".into(), "val".into())]);
331 }
332
333 #[test]
334 fn key_with_digits() {
335 assert_eq!(parse_ok("KEY123=val"), vec![("KEY123".into(), "val".into())]);
336 }
337
338 #[test]
339 fn key_mixed() {
340 assert_eq!(parse_ok("A1.b-C_2=val"), vec![("A1.b-C_2".into(), "val".into())]);
341 }
342
343 #[test]
344 fn key_starting_with_hyphen() {
345 assert_eq!(parse_ok("-KEY=v"), vec![("-KEY".into(), "v".into())]);
346 }
347
348 #[test]
349 fn key_starting_with_dot() {
350 assert_eq!(parse_ok(".KEY=v"), vec![(".KEY".into(), "v".into())]);
351 }
352
353 #[test]
354 fn key_starting_with_underscore() {
355 assert_eq!(parse_ok("_KEY=v"), vec![("_KEY".into(), "v".into())]);
356 }
357
358 #[test]
361 fn double_quoted_value() {
362 assert_eq!(parse_ok("K=\"hello\""), vec![("K".into(), "hello".into())]);
363 }
364
365 #[test]
366 fn double_quoted_with_spaces() {
367 assert_eq!(parse_ok("K=\"hello world\""), vec![("K".into(), "hello world".into())]);
368 }
369
370 #[test]
371 fn double_quoted_empty() {
372 assert_eq!(parse_ok("K=\"\""), vec![("K".into(), "".into())]);
373 }
374
375 #[test]
376 fn double_quoted_hash_preserved() {
377 assert_eq!(parse_ok("K=\"a#b\""), vec![("K".into(), "a#b".into())]);
378 }
379
380 #[test]
381 fn double_quoted_equals_inside() {
382 assert_eq!(parse_ok("K=\"a=b\""), vec![("K".into(), "a=b".into())]);
383 }
384
385 #[test]
386 fn double_quoted_single_quotes_inside() {
387 assert_eq!(parse_ok("K=\"it's ok\""), vec![("K".into(), "it's ok".into())]);
388 }
389
390 #[test]
391 fn double_quoted_whitespace_preserved() {
392 assert_eq!(parse_ok("K=\" hello \""), vec![("K".into(), " hello ".into())]);
393 }
394
395 #[test]
396 fn double_quoted_trailing_content_error() {
397 assert_eq!(parse_kind("K=\"hello\"extra"), ParseErrorKind::TrailingContent);
398 }
399
400 #[test]
401 fn double_quoted_trailing_comment_allowed() {
402 assert_eq!(parse_ok("K=\"hello\" # comment"), vec![("K".into(), "hello".into())]);
403 }
404
405 #[test]
408 fn single_quoted_value() {
409 assert_eq!(parse_ok("K='hello'"), vec![("K".into(), "hello".into())]);
410 }
411
412 #[test]
413 fn single_quoted_with_spaces() {
414 assert_eq!(parse_ok("K='hello world'"), vec![("K".into(), "hello world".into())]);
415 }
416
417 #[test]
418 fn single_quoted_empty() {
419 assert_eq!(parse_ok("K=''"), vec![("K".into(), "".into())]);
420 }
421
422 #[test]
423 fn single_quoted_hash_preserved() {
424 assert_eq!(parse_ok("K='a#b'"), vec![("K".into(), "a#b".into())]);
425 }
426
427 #[test]
428 fn single_quoted_double_quotes_inside() {
429 assert_eq!(parse_ok(r#"K='"hello"'"#), vec![("K".into(), r#""hello""#.into())]);
430 }
431
432 #[test]
433 fn single_quoted_whitespace_preserved() {
434 assert_eq!(parse_ok("K=' hello '"), vec![("K".into(), " hello ".into())]);
435 }
436
437 #[test]
438 fn single_quoted_trailing_content_error() {
439 assert_eq!(parse_kind("K='hello'extra"), ParseErrorKind::TrailingContent);
440 }
441
442 #[test]
445 fn quoted_nested_example() {
446 assert_eq!(parse_ok("HELLO='\"hello\"'"), vec![("HELLO".into(), "\"hello\"".into())]);
447 }
448
449 #[test]
452 fn unquoted_hash_is_comment() {
453 assert_eq!(parse_ok("K=val # comment"), vec![("K".into(), "val".into())]);
454 }
455
456 #[test]
457 fn unquoted_hash_no_space_not_comment() {
458 assert_eq!(parse_ok("K=val#comment"), vec![("K".into(), "val#comment".into())]);
459 }
460
461 #[test]
462 fn unquoted_trimmed() {
463 assert_eq!(parse_ok("K= val "), vec![("K".into(), "val".into())]);
464 }
465
466 #[test]
467 fn unquoted_trailing_spaces_before_comment() {
468 assert_eq!(parse_ok("K=val # comment"), vec![("K".into(), "val".into())]);
469 }
470
471 #[test]
472 fn unquoted_value_with_numbers() {
473 assert_eq!(parse_ok("PORT=8080"), vec![("PORT".into(), "8080".into())]);
474 }
475
476 #[test]
477 fn unquoted_value_with_dots() {
478 assert_eq!(parse_ok("HOST=192.168.1.1"), vec![("HOST".into(), "192.168.1.1".into())]);
479 }
480
481 #[test]
482 fn unquoted_value_containing_quote() {
483 assert_eq!(parse_ok("K=hello\"there"), vec![("K".into(), "hello\"there".into())]);
484 }
485
486 #[test]
487 fn unquoted_value_containing_only_hash() {
488 assert_eq!(parse_ok("K=#"), vec![("K".into(), "#".into())]);
489 }
490
491 #[test]
492 fn unquoted_value_hash_without_preceding_space() {
493 assert_eq!(parse_ok("K=val#ue"), vec![("K".into(), "val#ue".into())]);
494 }
495
496 #[test]
497 fn unquoted_hash_with_preceding_space_is_comment() {
498 assert_eq!(parse_ok("K=val #ue"), vec![("K".into(), "val".into())]);
499 }
500
501 #[test]
504 fn empty_value_no_quotes() {
505 assert_eq!(parse_ok("K="), vec![("K".into(), "".into())]);
506 }
507
508 #[test]
509 fn empty_value_trailing_spaces() {
510 assert_eq!(parse_ok("K= "), vec![("K".into(), "".into())]);
511 }
512
513 #[test]
514 fn empty_value_spaces_before_comment() {
515 assert_eq!(parse_ok("K= # comment"), vec![("K".into(), "".into())]);
516 }
517
518 #[test]
521 fn leading_whitespace_on_line() {
522 assert_eq!(parse_ok(" K=v"), vec![("K".into(), "v".into())]);
523 }
524
525 #[test]
526 fn trailing_whitespace_before_equals() {
527 assert_eq!(parse_ok("K =v"), vec![("K".into(), "v".into())]);
528 }
529
530 #[test]
531 fn whitespace_around_equals() {
532 assert_eq!(parse_ok("K = v"), vec![("K".into(), "v".into())]);
533 }
534
535 #[test]
536 fn tabs_as_whitespace() {
537 assert_eq!(parse_ok("\tK\t=\tv"), vec![("K".into(), "v".into())]);
538 }
539
540 #[test]
543 fn full_line_comment() {
544 assert!(parse_ok("# this is a comment").is_empty());
545 }
546
547 #[test]
548 fn comment_with_leading_spaces() {
549 assert!(parse_ok(" # indented comment").is_empty());
550 }
551
552 #[test]
553 fn empty_lines_skipped() {
554 assert!(parse_ok("\n\n\n").is_empty());
555 }
556
557 #[test]
558 fn mixed_comments_and_values() {
559 let pairs = parse_ok("# header\nA=1\n\nB=2 # inline\n");
560 assert_eq!(pairs, vec![("A".into(), "1".into()), ("B".into(), "2".into())]);
561 }
562
563 #[test]
566 fn unix_line_endings() {
567 assert_eq!(parse_ok("A=1\nB=2"), vec![("A".into(), "1".into()), ("B".into(), "2".into())]);
568 }
569
570 #[test]
571 fn windows_line_endings() {
572 assert_eq!(parse_ok("A=1\r\nB=2"), vec![("A".into(), "1".into()), ("B".into(), "2".into())]);
573 }
574
575 #[test]
576 fn no_trailing_newline() {
577 assert_eq!(parse_ok("A=1"), vec![("A".into(), "1".into())]);
578 }
579
580 #[test]
581 fn single_line_no_newline() {
582 assert_eq!(parse_ok("K=v"), vec![("K".into(), "v".into())]);
583 }
584
585 #[test]
588 fn empty_file() {
589 assert!(parse_ok("").is_empty());
590 }
591
592 #[test]
593 fn only_comments() {
594 assert!(parse_ok("# a\n# b\n# c").is_empty());
595 }
596
597 #[test]
598 fn only_blank_lines() {
599 assert!(parse_ok("\n\n \n\t\n").is_empty());
600 }
601
602 #[test]
605 fn error_missing_equals() {
606 assert_eq!(parse_kind("INVALID"), ParseErrorKind::MissingEquals);
607 }
608
609 #[test]
610 fn error_missing_equals_with_comment() {
611 assert_eq!(parse_kind("K # comment"), ParseErrorKind::MissingEquals);
612 }
613
614 #[test]
615 fn error_empty_key() {
616 assert_eq!(parse_kind("=value"), ParseErrorKind::EmptyKey);
617 }
618
619 #[test]
620 fn error_empty_key_with_spaces() {
621 assert_eq!(parse_kind(" =value"), ParseErrorKind::EmptyKey);
622 }
623
624 #[test]
625 fn error_unmatched_double_quote() {
626 assert_eq!(parse_kind("K=\"hello"), ParseErrorKind::UnmatchedQuote);
627 }
628
629 #[test]
630 fn error_unmatched_single_quote() {
631 assert_eq!(parse_kind("K='hello"), ParseErrorKind::UnmatchedQuote);
632 }
633
634 #[test]
635 fn error_unmatched_double_quote_with_hash() {
636 assert_eq!(parse_kind("K=\"hello#more"), ParseErrorKind::UnmatchedQuote);
637 }
638
639 #[test]
640 fn error_trailing_content_double_quote() {
641 assert_eq!(parse_kind("K=\"hello\"extra"), ParseErrorKind::TrailingContent);
642 }
643
644 #[test]
645 fn error_trailing_content_single_quote() {
646 assert_eq!(parse_kind("K='hello'extra"), ParseErrorKind::TrailingContent);
647 }
648
649 #[test]
650 fn error_trailing_content_line_number() {
651 assert_eq!(parse_line("A=1\nK=\"v\"x\nB=2"), 2);
652 }
653
654 #[test]
655 fn error_invalid_key_exclamation() {
656 assert_eq!(parse_kind("K!EY=v"), ParseErrorKind::InvalidKey);
657 }
658
659 #[test]
660 fn error_invalid_key_dollar() {
661 assert_eq!(parse_kind("\u{0024}KEY=v"), ParseErrorKind::InvalidKey);
662 }
663
664 #[test]
665 fn error_invalid_key_at() {
666 assert_eq!(parse_kind("KEY@=v"), ParseErrorKind::InvalidKey);
667 }
668
669 #[test]
670 fn error_invalid_key_space() {
671 assert_eq!(parse_kind("K EY=v"), ParseErrorKind::InvalidKey);
672 }
673
674 #[test]
675 fn error_invalid_key_slash() {
676 assert_eq!(parse_kind("KEY/VAL=v"), ParseErrorKind::InvalidKey);
677 }
678
679 #[test]
680 fn error_invalid_key_unicode() {
681 assert_eq!(parse_kind("K\u{00C9}Y=v"), ParseErrorKind::InvalidKey);
682 }
683
684 #[test]
685 fn error_line_number_missing_equals() {
686 assert_eq!(parse_line("A=1\nINVALID\nB=2"), 2);
687 }
688
689 #[test]
690 fn error_line_number_invalid_key() {
691 assert_eq!(parse_line("A=1\n\"$\"BAD=v\nB=2"), 2);
692 }
693
694 #[test]
695 fn error_line_number_unmatched_quote() {
696 assert_eq!(parse_line("A=1\nK=\"unclosed\nB=2"), 2);
697 }
698
699 #[test]
702 fn unicode_value_unquoted() {
703 assert_eq!(parse_ok("K=h\u{00E9}llo"), vec![("K".into(), "h\u{00E9}llo".into())]);
704 }
705
706 #[test]
707 fn unicode_value_double_quoted() {
708 assert_eq!(parse_ok("K=\"h\u{00E9}llo\""), vec![("K".into(), "h\u{00E9}llo".into())]);
709 }
710
711 #[test]
712 fn unicode_value_single_quoted() {
713 assert_eq!(parse_ok("K='h\u{00E9}llo'"), vec![("K".into(), "h\u{00E9}llo".into())]);
714 }
715
716 #[test]
719 fn load_sets_vars() {
720 let dir = env::temp_dir().join(format!("dotenv_test_{}", std::process::id()));
721 let _ = fs::create_dir_all(&dir);
722 let env_path = dir.join(".env");
723 fs::write(&env_path, "DOTENV_TEST_FOO=bar\nDOTENV_TEST_BAZ=qux").unwrap();
724
725 let old = env::current_dir().ok();
726 env::set_current_dir(&dir).unwrap();
727
728 let result = load();
729
730 if let Some(p) = old {
731 let _ = env::set_current_dir(p);
732 }
733 let _ = fs::remove_file(&env_path);
734 let _ = fs::remove_dir(&dir);
735
736 assert!(result.is_ok());
737 assert_eq!(env::var("DOTENV_TEST_FOO").unwrap(), "bar");
738 assert_eq!(env::var("DOTENV_TEST_BAZ").unwrap(), "qux");
739
740 unsafe { remove_env("DOTENV_TEST_FOO") };
741 unsafe { remove_env("DOTENV_TEST_BAZ") };
742 }
743
744 #[test]
745 fn load_preserves_existing_env_vars() {
746 unsafe { set_env("DOTENV_EXISTING", "original") };
747
748 let dir = env::temp_dir().join(format!("dotenv_test_existing_{}", std::process::id()));
749 let _ = fs::create_dir_all(&dir);
750 let env_path = dir.join(".env");
751 fs::write(&env_path, "DOTENV_EXISTING=from_file").unwrap();
752
753 let old = env::current_dir().ok();
754 env::set_current_dir(&dir).unwrap();
755
756 let result = load();
757
758 if let Some(p) = old {
759 let _ = env::set_current_dir(p);
760 }
761 let _ = fs::remove_file(&env_path);
762 let _ = fs::remove_dir(&dir);
763
764 assert!(result.is_ok());
765 assert_eq!(env::var("DOTENV_EXISTING").unwrap(), "original");
766
767 unsafe { remove_env("DOTENV_EXISTING") };
768 }
769
770 #[test]
771 fn load_first_declaration_wins() {
772 let dir = env::temp_dir().join(format!("dotenv_test_first_{}", std::process::id()));
773 let _ = fs::create_dir_all(&dir);
774 let env_path = dir.join(".env");
775 fs::write(&env_path, "DOTENV_DUP=first\nDOTENV_DUP=second").unwrap();
776
777 let old = env::current_dir().ok();
778 env::set_current_dir(&dir).unwrap();
779
780 let result = load();
781
782 if let Some(p) = old {
783 let _ = env::set_current_dir(p);
784 }
785 let _ = fs::remove_file(&env_path);
786 let _ = fs::remove_dir(&dir);
787
788 assert!(result.is_ok());
789 assert_eq!(env::var("DOTENV_DUP").unwrap(), "first");
790
791 unsafe { remove_env("DOTENV_DUP") };
792 }
793
794 #[test]
795 fn load_file_not_found() {
796 let dir = env::temp_dir().join(format!("dotenv_test_missing_{}", std::process::id()));
797 let _ = fs::create_dir_all(&dir);
798
799 let old = env::current_dir().ok();
800 env::set_current_dir(&dir).unwrap();
801
802 let result = load();
803
804 if let Some(p) = old {
805 let _ = env::set_current_dir(p);
806 }
807 let _ = fs::remove_dir(&dir);
808
809 match result.unwrap_err() {
810 Error::Io(_) => {}
811 _ => panic!("expected Io error"),
812 }
813 }
814
815 #[test]
816 fn load_parse_error() {
817 let dir = env::temp_dir().join(format!("dotenv_test_parse_err_{}", std::process::id()));
818 let _ = fs::create_dir_all(&dir);
819 let env_path = dir.join(".env");
820 fs::write(&env_path, "A=1\nMALFORMED\nB=2").unwrap();
821
822 let old = env::current_dir().ok();
823 env::set_current_dir(&dir).unwrap();
824
825 let result = load();
826
827 if let Some(p) = old {
828 let _ = env::set_current_dir(p);
829 }
830 let _ = fs::remove_file(&env_path);
831 let _ = fs::remove_dir(&dir);
832
833 match result.unwrap_err() {
834 Error::Parse(e) => assert_eq!(e.line, 2),
835 _ => panic!("expected Parse error"),
836 }
837 }
838}