dotenv_verbatim/lib.rs
1//! A `.env` loader that takes the value verbatim: everything after the first `=`, with no
2//! expansion, no escape processing and no inline-comment stripping.
3//! A line that cannot be a key/value pair is skipped by number, never the rest of the file, and a
4//! variable already present in the process environment wins. Rationale and evidence: README.
5
6#![forbid(unsafe_code)]
7
8mod parse;
9
10pub use crate::parse::parse;
11pub use crate::parse::Entry;
12pub use crate::parse::Parsed;
13
14use std::path::Path;
15
16/// What the load did. Names are owned: the report outlives the file's content.
17/// `Default` is exactly the missing-file report: nothing found, nothing applied.
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
19pub struct Loaded {
20 /// The file was read. False means it was absent or unreadable, which is not an error.
21 pub found: bool,
22 /// Variables this load set, in file order.
23 pub applied: Vec<String>,
24 /// Declared by the file, not set: the process already carried the variable.
25 pub overridden: Vec<String>,
26 /// Declared more than once in the file; the later line lost to the earlier one.
27 pub repeated: Vec<String>,
28 /// Line numbers, from 1, that were neither a pair nor a comment or blank — the same
29 /// numbers `parse` reports.
30 pub skipped: Vec<usize>,
31}
32
33/// Read the file, set every variable it declares that the process does not have yet, and report
34/// what became of every entry, so a caller can say which line of the file is not in force and why.
35/// A missing or unreadable file is not an error: the deployed environment provides real vars.
36/// A key repeated inside the file keeps its first value; the later line is reported as repeated.
37pub fn load(path: &Path) -> Loaded {
38 let content = match std::fs::read_to_string(path) {
39 Ok(content) => content,
40 Err(_) => return Loaded::default(),
41 };
42 let parsed = parse(&content);
43 let mut loaded = Loaded {
44 found: true,
45 skipped: parsed.skipped,
46 ..Loaded::default()
47 };
48 for entry in parsed.entries {
49 match fate_of(entry.key, &loaded.applied, &loaded.overridden) {
50 Fate::Repeated => loaded.repeated.push(entry.key.to_owned()),
51 Fate::Overridden => loaded.overridden.push(entry.key.to_owned()),
52 Fate::Applied => {
53 std::env::set_var(entry.key, entry.value);
54 loaded.applied.push(entry.key.to_owned());
55 }
56 }
57 }
58 loaded
59}
60
61/// What one entry of the file turned out to be for this load.
62enum Fate {
63 /// The file already declared this name earlier in the same pass.
64 Repeated,
65 /// The process carries the variable, so the file's value never takes effect.
66 Overridden,
67 /// The load sets the variable from the file.
68 Applied,
69}
70
71/// Decide an entry's fate without changing anything. The file's own repetition is checked first:
72/// on the second occurrence the variable is set only because of the first line, which is a
73/// different fact for a reader than a value coming from the process environment.
74fn fate_of(key: &str, applied: &[String], overridden: &[String]) -> Fate {
75 if contains_name(applied, key) || contains_name(overridden, key) {
76 return Fate::Repeated;
77 }
78 // dotenv semantics: a variable already in the process environment wins over the file.
79 if std::env::var_os(key).is_some() {
80 return Fate::Overridden;
81 }
82 Fate::Applied
83}
84
85fn contains_name(names: &[String], key: &str) -> bool {
86 names.iter().any(|name| name == key)
87}