Skip to main content

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}