# 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.
```rust
// 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 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.**
```env
BASE_ADDRESSES=http://a http://b application
AFTER=1
```
```text
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.**
```env
PLAIN=abc
SECRET=p$word${PLAIN}x
```
```text
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.**
```env
HASH=a#b
```
```text
dotenvs -> ("HASH", "a")
```
**4. A malformed line silently discards everything after it.**
```env
A=1
nosep
B=2
```
```text
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 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=c` keeps `a=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 (`\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.
```rust
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 loader in this
space has the same constraint.
## License
MIT OR Apache-2.0