dotenv-verbatim 0.1.0

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
  • Coverage
  • 100%
    9 out of 9 items documented0 out of 1 items with examples
  • Size
  • Source code size: 27.23 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 268.45 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 2s Average build duration of successful builds.
  • all releases: 3s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • MarketKernel/dotenv-verbatim
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • MarketKernel

dotenv-verbatim

A .env loader that takes the value verbatim: everything after the first =, with no expansion, no escape processing, and no inline-comment stripping. One malformed line is skipped, never the rest of the file. A variable already present in the process environment always wins.

// At startup, before any thread or task is spawned.
dotenv_verbatim::load(Path::new(".env"));

Why this exists

It was written because the established crates corrupt or drop real configuration files. Every example below was run against dotenvy 0.15.7 and dotenvs 0.2.2 (the dotenv 0.15 crate is unmaintained since 2019; dotenv-parser 0.1.3 behaves like dotenvy on these inputs).

1. An unquoted value containing a space aborts the whole file.

BASE_ADDRESSES=http://a http://b application
AFTER=1
dotenvy::from_path(".env")  ->  Err(LineParse("http://a http://b application", 9))
std::env::var("AFTER")      ->  Err(NotPresent)

The parser rejects the line, and from_path stops there, so nothing at all is loaded, including the lines that were fine. A=b c is enough to trigger it. Space-separated lists are ordinary in deployed .env files, and quoting them everywhere is a migration across every machine that has one.

2. $ in a value is silently rewritten.

PLAIN=abc
SECRET=p$word${PLAIN}x
dotenvy  ->  ("SECRET", "pabcx")
dotenvs  ->  ("SECRET", "pabcx")

$word is an undefined variable, so it expands to nothing; ${PLAIN} expands to its value. A password, a token, or a hash containing $ reaches the application quietly mangled, and no error says so. Related: dotenvy rejects Q="q$PLAIN" outright with LineParse.

3. # truncates a value.

HASH=a#b
dotenvs  ->  ("HASH", "a")

4. A malformed line silently discards everything after it.

A=1
nosep
B=2
dotenvs::from_path(".env")  ->  Ok, but the iterator yields only ("A", "1")

No error is returned. Half the configuration is gone, and the process starts with defaults for the missing half.

This crate makes the opposite choices. The value is whatever follows the first =, trimmed, with at most one pair of matching surrounding quotes removed. $, #, spaces, and = inside a value are data. A line that cannot be a key/value pair is skipped and reported by line number, and parsing continues. A key or value containing a NUL byte is skipped the same way, because set_var panics on one.

What it does support

  • KEY=value, with an optional export prefix.
  • Blank lines and whole-line # comments.
  • Surrounding " or ' removed as one pair; anything inside is untouched.
  • Leading and trailing whitespace around key and value trimmed.
  • KEY=a=b=c keeps a=b=c.
  • CRLF line endings.
  • A missing file is not an error: the deployed environment is expected to provide real variables.

What it deliberately does not support

  • Variable expansion ($VAR, ${VAR}).
  • Escape sequences inside quotes (\n stays two characters).
  • Inline comments after a value.
  • Multi-line quoted values.

Each one exists to keep a value byte-identical to what the file says.

Parsing without touching the environment

parse is pure and is where the tests live. It returns the entries in file order plus the line numbers that were skipped, so a caller can log them.

let parsed = dotenv_verbatim::parse(&content);
for entry in &parsed.entries { /* ... */ }
for line in &parsed.skipped { eprintln!(".env:{line}: not a KEY=VALUE line, skipped"); }

A note on set_var

load calls std::env::set_var, which is unsound while another thread reads the environment. Call it once at startup, before spawning threads or tasks. This crate is Rust edition 2021, where set_var is a safe function, so a caller on edition 2024 gets no unsafe block of its own; that is a lint difference, not a soundness one, and the rule above still holds. Every crate in this space has the same constraint.

License

MIT OR Apache-2.0