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.
load;
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 also fails these files, but differently: it rejects
examples 1 and 4 outright and truncates at #, while keeping $ verbatim.
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 optionalexportprefix followed by whitespace.- 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=ckeepsa=b=c.- CRLF line endings, and a leading UTF-8 BOM.
- A key repeated in one file: the first occurrence wins, as in dotenvy and dotenvs without override.
- 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 (
\nstays 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 = parse;
for entry in &parsed.entries
for line in &parsed.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 loader in this
space has the same constraint.
License
MIT OR Apache-2.0