adotenv
A literal, lossless .env parser and editing library for Rust.
adotenv is built for linters, formatters, editors, scanners and codebase maintenance tools. It keeps source text exactly as written, reports precise locations and verifies every edit before returning it.
What it does
- Values stay literal.
${HOME},$USERand command-like text are never expanded or executed by the parser. - Any UTF-8 source round-trips byte for byte, including invalid statements, comments, spacing, a BOM and mixed line endings.
- Every token has a source span. Decoded values map back to the original bytes.
- Edits return source-relative text edits and a re-parsed, verified result.
- The core has no runtime dependencies, no file I/O, no environment access and
no process spawning.
#![forbid(unsafe_code)]is unconditional. - Filesystem loading, process mutation and command execution are separate opt-in features or crates.
Install
[]
= "0.10.2"
| Feature | Purpose |
|---|---|
default (std) |
Error traits and standard desktop use |
load |
Read .env cascades from files |
serde |
Deserialize documents and loaded environments |
exec |
Run command substitutions with an explicit directory |
Disable default features for a no_std core that uses alloc:
= { = "0.10.2", = false }
Parse
let text = "# app\nexport PORT=8080\nURL=\"${HOST}/api\" # base\n";
let doc = parse_strict.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
let url = doc.get_entry.unwrap;
assert_eq!;
assert_eq!;
let edited = doc.set.unwrap;
assert_eq!;
parse_strict returns the first error. parse always returns a document and a
list of diagnostics, so callers can inspect broken files without losing source:
let = parse;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
Diagnostics have stable codes, severities, one-based lines and primary or secondary spans. Match codes and spans, not message text.
parse_owned(String) moves the source into the document. Document::into_owned
copies only when the document still borrows its input.
Dialects
The default is Dialect::PORTABLE. Other presets are NODE_DOTENV,
PYTHON_DOTENV, COMPOSE and DOCKER_RAW. Presets choose parsing rules only.
They do not enable interpolation, inheritance or loading.
use ;
let source = "A=a#b\nB=`two words`\n";
let = parse_with;
assert_eq!;
assert_eq!;
let = node.reparse;
assert_eq!;
assert_eq!;
let custom = NODE_DOTENV.with_inline_comments;
assert_eq!;
Each document retains its exact configuration. reparse borrows the same source
and rebuilds items, entries and source maps for another dialect. Snapshot IDs do
not carry across interpretations.
Lookups and raw tokens
Duplicate declarations are retained. get_entry selects the effective
declaration, get_all returns every occurrence and unique rejects ambiguity.
let doc = parse_strict.unwrap;
assert_eq!;
assert_eq!;
assert!;
let index = doc.lookup_index;
assert_eq!;
assert_eq!;
let winners: = doc
.effective_entries
.map
.collect;
assert_eq!;
Raw tokens remain available:
let source = "export A=\"two words\" # note\n";
let doc = parse_strict.unwrap;
let entry = doc.unique.unwrap;
assert!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
References and resolution
Entry::references reports $VAR, ${VAR:-default} and $(command) candidates
without expanding them. Each reference records spans, quote and escape context,
operators and nested forms.
use ;
let source = "A=${HOST:-localhost}\n";
let doc = parse_strict.unwrap;
let reference = &doc.unique.unwrap.references;
assert_eq!;
assert_eq!;
assert!;
assert!;
Resolution uses document bindings plus a caller lookup closure. It never reads the environment, files or commands on its own.
use ;
let doc = parse_strict.unwrap;
let resolved = doc.resolve.unwrap;
assert_eq!;
let caller = doc
.resolve
.unwrap;
assert_eq!;
Commands run only through an explicit callback or the opt-in exec feature:
use ;
let doc = parse_strict.unwrap;
let resolved = doc
.resolve_with_commands
.unwrap;
assert_eq!;
assert_eq!;
Format and sort
Document::format changes only requested trivia and verifies the complete result
with the document dialect.
use ;
let source = "# app \n\nA=1 \nB = 2 # note \n";
let doc = parse_strict.unwrap;
let formatted = doc.format.unwrap;
assert_eq!;
assert!;
Document::sort orders declarations inside blank-line-separated groups and
keeps comments and multiline entries intact. References inside a group are
placed before their dependents. Groups with duplicates, empty keys, malformed
items or dependency cycles are left unchanged.
let doc = parse_strict.unwrap;
assert_eq!;
Compare and generate examples
use ;
let left = parse_strict.unwrap;
let right = parse_strict.unwrap;
let comparison = left.compare;
let changes: = comparison
.differences
.iter
.map
.collect;
assert_eq!;
Document::example removes assigned values while preserving layout. Malformed
statements and empty-key entries become comments so they cannot leak values.
let source = "# app\nA=1\nB=\"two\" # note\nBARE\nBAD KEY=secret\n";
assert_eq!;
Edit a snapshot
Every edit method returns VerifiedEdits. Ranges refer to the original source.
Apply them from the highest offset to the lowest.
use ;
let doc = parse_strict.unwrap;
let second = doc.get_all.nth.unwrap;
let verified = doc
.edit
.unwrap;
let = parse;
assert_eq!;
assert_eq!;
assert!;
set, remove, rename, insert, comment, uncomment, set_export and
edit verify the whole batch and reject changes that alter unrelated syntax.
Use encode_value_with(value, dialect) when you need a standalone literal token:
use ;
let value = "literal #\0\n";
let token = encode_value_with.unwrap;
let source = format!;
assert_eq!;
assert!;
Positions
Line and column conversions are explicit and checked.
use ;
let doc = parse_strict.unwrap;
let index = doc.line_index;
let position = index.to_position.unwrap;
assert_eq!;
assert_eq!;
ColumnEncoding supports UTF-8 bytes, Unicode scalars and UTF-16 code units.
Out-of-range positions and split surrogates return an error instead of being
clamped. Entry and diagnostic lines are one-based; Position is zero-based.
Load files
Enable the load feature to read a cascade from an explicit directory. The order
is .env, .env.local, .env.{mode} and .env.{mode}.local. Later files win
by effective key. Missing cascade files are skipped. Read, UTF-8 and syntax
errors invalidate the whole load.
use load_cascade;
let loaded = load_cascade?;
assert_eq!;
let origin = loaded.origin.unwrap;
println!;
The default loader keeps values, shared-file origins and raw winning tokens. Reports are opt-in:
use ;
let loaded = load_cascade_with_options?;
for entry in loaded.entries
LoadOptions::new(dialect) is the default profile. Builders can disable raw
tokens, origins or reports. load_values and load_cascade_values retain only
final strings.
LoadedEnvironment::resolve_contextual resolves merged values with their original
quote and escape context. resolve_decoded treats final values as unquoted text.
Process environment
adotenv-process applies a loaded environment to the current process. The API
is unsafe because the process environment is global and not thread-safe on all
platforms. Validation happens before the first mutation.
use load_cascade;
use apply;
let loaded = load_cascade?;
// The caller must guarantee no concurrent environment access.
unsafe ;
Prefer iterating LoadedEnvironment::values() when mutation is not required.
Serde
With the serde feature, documents and loaded environments deserialize into
typed values:
use Deserialize;
let doc = parse_strict.unwrap;
let config: Config = doc.deserialize?;
Only strings, booleans, numbers and characters are converted from literal values. Nested structures and enum layouts are not invented. Errors carry the source line and key.
CLI
The workspace includes adotenv-cli:
get <file> <key>
check [--format human|json|github] [--source] [--recursive] <path>...
fmt [--write|--check] [--sort] [--recursive] <path>...
diff [--format human|matrix|json] <baseline> <file>...
example <file>
load <directory> [mode]
set <file> <key> <value>
run <directory> [mode] -- <program> [args...]
check --format json and diff --format json use versioned machine schemas.
check --format github emits workflow annotations. Human output omits source
lines unless --source is passed.
run starts a child with loaded values. It never mutates the parent process.
See CLI details.
Adapters
adotenv-config exposes literal values as a config source. adotenv-figment
exposes a Figment dictionary. Both support loaded environments through their
load feature.
use parse_strict;
use AdotenvSource;
let doc = parse_strict.unwrap;
let config = builder
.add_source
.build?;
Design limits
- Input is UTF-8. Decode bytes before parsing; invalid UTF-8 is rejected rather than replaced.
- Parsing does not read files, environment variables or commands.
- Retained documents keep token and item metadata. Apply byte and line budgets for untrusted input.
- Windows writes use a direct replace when possible and a backup swap otherwise. The fallback is not crash-atomic.
- The
configintegration usesconfig0.14.
Rust version
Rust 1.81, edition 2021. Development and fuzz dependencies may require a newer toolchain.
License
Licensed under either Apache License, Version 2.0 or MIT license, at your option.