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
# 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, and
`load` reports which of the file's declarations that leaves out of force.

```rust
// At startup, before any thread or task is spawned.
let loaded = 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.
- A variable the process already carries keeps its value; the file's line is reported as
  `overridden` rather than applied.

## 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.

## What the load reports

`load` returns a `Loaded`, so a caller can answer "which line of the file is not in force, and
why" without reading the file a second time and reimplementing the priority rule. The loader is the
only code that knows what actually got set.

```rust
let loaded = dotenv_verbatim::load(Path::new(".env"));
for key in &loaded.overridden { println!("{key}: from the process environment, not from .env"); }
for key in &loaded.repeated { println!("{key}: declared twice in .env, the first line is in force"); }
for line in &loaded.skipped { eprintln!(".env:{line}: not a KEY=VALUE line, skipped"); }
```

- `found` — the file was read. False means absent or unreadable, which is not an error; every list
  is then empty.
- `applied` — the variables this load set, in file order.
- `overridden` — declared by the file, not set: the process already carried the variable.
- `repeated` — declared more than once in the file; the later line lost to the earlier one.
- `skipped` — line numbers, from 1, that were neither a pair nor a comment or blank.

`overridden` and `repeated` are separate because they are different facts for whoever reads the
diagnostics: one says the deployment supplies the value, the other says the file contradicts
itself. An empty value stays a set empty string — treating "empty" as "unset" is the consumer's
call, not the loader's.

## 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