dotenv-verbatim 0.3.1

A .env loader that takes the value verbatim: no expansion, no escapes, no inline comments; one malformed line is skipped, not the rest of the file
Documentation
//! A `.env` loader that takes the value verbatim: everything after the first `=`, with no
//! expansion, no escape processing and no inline-comment stripping.
//! A line that cannot be a key/value pair is skipped by number, never the rest of the file, and a
//! variable already present in the process environment wins. Rationale and evidence: README.

#![forbid(unsafe_code)]

mod parse;

pub use crate::parse::parse;
pub use crate::parse::Entry;
pub use crate::parse::Parsed;

use std::path::Path;

/// What the load did. Names are owned: the report outlives the file's content.
/// `Default` is exactly the missing-file report: nothing found, nothing applied.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Loaded {
    /// The file was read. False means it was absent or unreadable, which is not an error.
    pub found: bool,
    /// Variables this load set, in file order.
    pub applied: Vec<String>,
    /// Declared by the file, not set: the process already carried the variable.
    pub overridden: Vec<String>,
    /// Declared more than once in the file; the later line lost to the earlier one.
    pub repeated: Vec<String>,
    /// Line numbers, from 1, that were neither a pair nor a comment or blank — the same
    /// numbers `parse` reports.
    pub skipped: Vec<usize>,
}

/// Read the file, set every variable it declares that the process does not have yet, and report
/// what became of every entry, so a caller can say which line of the file is not in force and why.
/// A missing or unreadable file is not an error: the deployed environment provides real vars.
/// A key repeated inside the file keeps its first value; the later line is reported as repeated.
pub fn load(path: &Path) -> Loaded {
    let content = match std::fs::read_to_string(path) {
        Ok(content) => content,
        Err(_) => return Loaded::default(),
    };
    let parsed = parse(&content);
    let mut loaded = Loaded {
        found: true,
        skipped: parsed.skipped,
        ..Loaded::default()
    };
    for entry in parsed.entries {
        match fate_of(entry.key, &loaded.applied, &loaded.overridden) {
            Fate::Repeated => loaded.repeated.push(entry.key.to_owned()),
            Fate::Overridden => loaded.overridden.push(entry.key.to_owned()),
            Fate::Applied => {
                std::env::set_var(entry.key, entry.value);
                loaded.applied.push(entry.key.to_owned());
            }
        }
    }
    loaded
}

/// What one entry of the file turned out to be for this load.
enum Fate {
    /// The file already declared this name earlier in the same pass.
    Repeated,
    /// The process carries the variable, so the file's value never takes effect.
    Overridden,
    /// The load sets the variable from the file.
    Applied,
}

/// Decide an entry's fate without changing anything. The file's own repetition is checked first:
/// on the second occurrence the variable is set only because of the first line, which is a
/// different fact for a reader than a value coming from the process environment.
fn fate_of(key: &str, applied: &[String], overridden: &[String]) -> Fate {
    if contains_name(applied, key) || contains_name(overridden, key) {
        return Fate::Repeated;
    }
    // dotenv semantics: a variable already in the process environment wins over the file.
    if std::env::var_os(key).is_some() {
        return Fate::Overridden;
    }
    Fate::Applied
}

fn contains_name(names: &[String], key: &str) -> bool {
    names.iter().any(|name| name == key)
}