adotenv 0.9.7

A tooling-grade .env parser: literal values (no variable expansion), exact byte spans, and edits that leave the rest of the file untouched.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
# adotenv

A tooling-grade `.env` parser for Rust: literal values, exact byte spans, and
verified edits that leave unrelated source untouched.

[![crates.io](https://img.shields.io/crates/v/adotenv.svg)](https://crates.io/crates/adotenv)
[![docs.rs](https://img.shields.io/docsrs/adotenv)](https://docs.rs/adotenv)
[![CI](https://github.com/asamarts/adotenv/actions/workflows/ci.yml/badge.svg)](https://github.com/asamarts/adotenv/actions/workflows/ci.yml)

Most `.env` crates load a file into a process. `adotenv` is for linters,
formatters, editors, language servers, scanners and migration tools:

- **Literal values.** `${HOME}`, `$USER` and command-looking text are never
  expanded or executed. Results do not depend on the host environment.
- **Lossless recovery.** Any UTF-8 source is retained byte for byte, including
  invalid statements, comments, spacing, a BOM and mixed line endings.
- **Precise source locations.** Raw token spans, decoded-to-raw value mapping,
  and checked UTF-8, Unicode-scalar and UTF-16 positions address the original
  source, not a normalized copy.
- **Verified native edits.** Set, remove, rename, insert, comment, uncomment
  and export changes return source-relative text edits and an already-verified
  output. Unrelated entries and invalid syntax must keep their meaning.
  Trivia-only formatting returns the same kind of verified edits.
- **Comparison and examples.** Effective-key differences distinguish missing,
  bare, empty and assigned values. Example generation removes values while
  keeping comments and never copies malformed value text.
- **Small, side-effect-free core.** Zero runtime dependencies, no file I/O,
  environment access or process spawning by default, and unconditional
  `forbid(unsafe_code)`. The opt-in `load` feature is the only filesystem path
  and `exec` is the only process-spawning path. Disable the default `std`
  feature for `no_std` with `alloc`.

## Read a document

```rust
let text = "# app\nexport PORT=8080\nURL=\"${HOST}/api\" # base\n";
let doc = adotenv::parse_strict(text).unwrap();

assert_eq!(doc.get("PORT"), Some("8080"));
assert_eq!(doc.get("URL"), Some("${HOST}/api")); // literal, never expanded
let url = doc.get_entry("URL").unwrap();
assert_eq!(url.line(), 3); // explicitly one-based
assert_eq!(&text[url.value_span().unwrap()], "\"${HOST}/api\"");
assert_eq!(doc.to_string(), text);

let edited = doc.set("URL", "https://example.invalid").unwrap();
assert_eq!(adotenv::parse_strict(edited.as_str()).unwrap().get("URL"),
           Some("https://example.invalid"));
```

`parse` recovers instead of failing. Diagnostics have stable `DiagnosticCode`
values and `code().as_str()` identifiers, severity, primary and optional
secondary spans, and one-based line numbers. Match codes, not message wording.
`parse_strict` rejects the first **error**, not warnings or informational notes.

```rust
let (doc, diagnostics) = adotenv::parse("BAD KEY=value\nGOOD=retained\n");
assert_eq!(doc.get("GOOD"), Some("retained"));
assert_eq!(doc.items().next().unwrap().kind(), adotenv::ItemKind::Invalid);
assert_eq!(diagnostics[0].code(), adotenv::DiagnosticCode::InvalidKey);
assert_eq!(doc.source(), "BAD KEY=value\nGOOD=retained\n");

let (owned, _) = adotenv::parse_owned(String::from("A=value\n"));
assert_eq!(owned.get("A"), Some("value")); // owns its source, no self-references
```

`entries()` and `items()` are exact-size, double-ended **view iterators**.
Use `entry(id)` and `item(id)` for indexed access; IDs are zero-based and local
only to that source snapshot. Borrowed parsing does not copy the source;
`parse_owned(String)` moves it, and `Document::into_owned()` copies only when
necessary. Decoded values allocate only when actual unescaping is required.

## Select a dialect (0.3)

`parse`, `parse_owned` and `parse_strict` still default to Portable.
`parse_with`, `parse_owned_with` and `parse_strict_with` take an explicit
`Dialect`: `PORTABLE`, `NODE_DOTENV`, `PYTHON_DOTENV`, `COMPOSE` or `DOCKER_RAW`.
Presets select literal syntax, never interpolation, inheritance or loading.

```rust
use adotenv::{parse_with, Dialect, InlineComments};

let source = "A=a#b\nB=`two words`\n";
let (node, _) = parse_with(source, Dialect::NODE_DOTENV);
assert_eq!(node.get("A"), Some("a"));
assert_eq!(node.get("B"), Some("two words"));
let (raw, _) = node.reparse(Dialect::DOCKER_RAW);
assert_eq!(raw.get("A"), Some("a#b"));
assert_eq!(raw.get("B"), Some("`two words`"));
assert_eq!(raw.source(), source);

let custom = Dialect::NODE_DOTENV.with_inline_comments(InlineComments::None);
assert_eq!(parse_with(source, custom).0.get("A"), Some("a#b"));
```

Each document retains its exact configuration. `reparse` borrows the original
source but rebuilds dialect-specific items, entries and source maps; snapshot
IDs must not be reused across interpretations. Typed fields control comments,
escapes, key syntax, bare declarations, quotes, raw values, duplicate lookup,
BOMs, line breaks and other grammar policies. See [dialect contracts and
upstream limitations](docs/design/dialects.md).

For repeated lookups, build an explicit borrowed index once with
`doc.lookup_index()` and call `index.get(key)` or `index.get_entry(key)`:

```rust
let doc = adotenv::parse_strict("A=1\nA=2\nB=3\n").unwrap();
let index = doc.lookup_index();
assert_eq!(index.get("A"), Some("2"));
assert_eq!(index.len(), 2);
```

`entries()` and `get_all()` retain declarations excluded from effective lookup.
`participates_in_lookup()` distinguishes ignored bare declarations and Node's
special `__proto__` property. `BareKeys::Empty` changes effective `get()` to an
empty string without changing a bare entry's `value() == None` or syntax state.
Malformed syntax ignored by a preset remains an `ItemKind::Ignored` item with
an `IgnoredStatement` warning; explicit forbidden-policy violations stay errors.

### Effective values and raw tokens

`effective_entries()` yields only winning declarations that participate in
lookup, while `get_all()` and `unique()` expose duplicate occurrences and
ambiguity. Raw tokens remain addressable through `EntryTokens` and the entry
span helpers:

```rust
let doc = adotenv::parse_strict("A=1\nA=2\nB\n").unwrap();
assert_eq!(doc.get_all("A").count(), 2);
assert!(doc.unique("A").is_err());
let winners: Vec<_> = doc
    .effective_entries()
    .map(|entry| (entry.key(), entry.value()))
    .collect();
assert_eq!(winners, [("A", "2")]);

let source = "export A=\"two words\" # note\n";
let doc = adotenv::parse_strict(source).unwrap();
let entry = doc.unique("A").unwrap();
assert!(entry.exported());
assert_eq!(&source[entry.key_span()], "A");
assert_eq!(entry.raw_value(), Some("\"two words\""));
assert_eq!(entry.value(), Some("two words"));
assert_eq!(entry.inline_comment(), Some("# note"));
assert!(entry.tokens().export.is_some());
assert_eq!(entry.decoded_span_to_raw(0..3).unwrap(), 10..13);
```

For 0.2 consumers, Portable parsing and the existing entry/edit APIs keep their
defaults. Code constructing `EntryTokens` must supply the new `quoted_tail`
field (`None` when absent). Under quoted-key presets, `EntryTokens::key` covers
the raw quote wrapper; `Entry::key_span()` covers only the semantic key.

## Check portability (0.3)

`Document::portability` reparses the retained source once for each unique
configuration and reports every physical line whose reading differs. The
document's own configuration is always included first. Declarations, decoded
values, assignment states, export prefixes, lookup participation, effective
lookup values, duplicate lookup winners and statement grouping are compared;
quote style alone is not a difference. No expansion, lookup or I/O occurs.

```rust
use adotenv::{parse_with, Dialect, PortabilityPart};

let (doc, _) = parse_with("A=a#b\n", Dialect::PORTABLE);
let report = doc.portability(&[Dialect::NODE_DOTENV]);
assert_eq!(report.len(), 1);
let difference = &report.differences()[0];
assert_eq!(difference.first_line, 1);
assert!(difference.readings[0].parts.iter().any(|part| {
    if let PortabilityPart::Declaration(declaration) = part {
        declaration.value.as_deref() == Some("a#b")
    } else {
        false
    }
}));
assert!(difference.readings[1].parts.iter().any(|part| {
    if let PortabilityPart::Declaration(declaration) = part {
        declaration.value.as_deref() == Some("a")
    } else {
        false
    }
}));
```

## Inspect references (0.4)

`Entry::references` reports raw `$VAR`, `${VAR:-default}` and `$(command)`
candidates in source order. Each candidate carries source spans, quote/escape
context, variable operators, nested containers and malformed-form metadata.
Escaped dollars, single-quoted text and unterminated forms remain observable;
they are not automatically eligible. `ReferenceRules` and `OperatorSet` make
eligibility explicit, with no lookup, expansion, I/O or command execution.

```rust
use adotenv::{parse_strict, ReferenceOperator, ReferenceRules};

let source = "A=${HOST:-localhost}\n";
let doc = parse_strict(source).unwrap();
let references = doc.unique("A").unwrap().references();
assert_eq!(references.len(), 1);
let reference = &references[0];
assert_eq!(&source[reference.span.clone()], "${HOST:-localhost}");
assert_eq!(
    reference.operator,
    Some(ReferenceOperator::DefaultIfUnsetOrEmpty)
);
assert_eq!(&source[reference.argument.clone().unwrap()], "localhost");
assert!(reference.is_eligible(ReferenceRules::COMPOSE));
assert!(!reference.is_eligible(ReferenceRules::PORTABLE));
```

## Resolve values (0.4)

`Document::resolve` substitutes eligible variable references using the
document's own bindings and an explicit caller lookup closure. It never reads
the process environment, files or commands. Caller values are literal and are
not re-expanded. `ExpandOptions` selects sequential or whole-file scope,
caller-first or file-first precedence, unresolved-variable policy, quote/escape
rules and expansion limits. Required `:?` variables, reachable cycles, depth and
size limits return structured `ResolveError` values; successful substitutions
record file or caller origins and nested file provenance.

```rust
use adotenv::{parse_strict, ExpandOptions};

let doc = parse_strict("BASE=base\nURL=${BASE}/api\n").unwrap();
let resolved = doc.resolve(|_| None, &ExpandOptions::portable()).unwrap();
assert_eq!(resolved.entries()[1].value.as_deref(), Some("base/api"));

let caller = doc
    .resolve(
        |key| (key == "BASE").then(|| "external".to_string()),
        &ExpandOptions::portable().with_lookup_order(adotenv::LookupOrder::CallerFirst),
    )
    .unwrap();
assert_eq!(caller.entries()[1].value.as_deref(), Some("external/api"));
```

Command substitutions and ineligible references remain literal in this slice.
No preset enables ambient environment lookup, loading or command execution.

### Command substitution by explicit callback

Commands never run by default. `resolve_with_commands` accepts a caller-supplied
callback only when the selected rules enable command substitutions. The callback
receives the raw command text, the whole reference and payload spans, and an
optional caller-supplied source identity. Outputs are literal; every call is
recorded, and a failure retains calls already made.

For trusted files, the optional `exec` feature provides `exec::Shell` with an
explicit working directory and optional source-identity-to-directory map. It
runs commands through the platform shell, trims one trailing newline and never
falls back to the host working directory. It is never enabled by default.

```rust
use adotenv::{parse_strict, ExpandOptions};

let doc = parse_strict("A=$(echo hi)\n").unwrap();
let resolved = doc
    .resolve_with_commands(
        |_| None,
        |context| Ok(format!("ran:{}", context.command)),
        Some("synthetic.env"),
        &ExpandOptions::dotenv_expand(),
    )
    .unwrap();
assert_eq!(resolved.entries()[0].value.as_deref(), Some("ran:echo hi"));
assert_eq!(resolved.commands().len(), 1);
```

## Inspect structure (0.5)

`Document::groups` partitions nonblank items on blank lines. `Group::entries`,
`Group::comments` and `Group::span` inspect one run without copying source.
`Document::header` is the structural prefix before the first declaration.
`Entry::leading_comments` returns the contiguous whole-line comments directly
above an entry; a blank line breaks attachment. Inline comments remain available
through `Entry::inline_comment`.

```rust
use adotenv::parse_strict;

let source = "# header\nA=1\n# note for B\nB=2\n\nC=3\n";
let doc = parse_strict(source).unwrap();

let header = doc.header().unwrap();
assert_eq!(header.entries().count(), 0);
assert_eq!(&source[header.span()], "# header\n");

let groups: Vec<_> = doc.groups().collect();
assert_eq!(groups.len(), 2);
assert_eq!(groups[0].entries().count(), 2);
assert_eq!(groups[0].leading_comments().count(), 1);

let notes: Vec<_> = doc.unique("B").unwrap().leading_comments().collect();
assert_eq!(notes[0].raw(), "# note for B\n");
```

Groups and headers are views over the selected dialect's items. They do not
guess comment ownership, move comments, or perform expansion.

## Format whitespace (0.5)

`Document::format` changes only selected syntax trivia: spacing around
assignment separators, trailing spaces/tabs and the final newline. It reparses the complete result with
the document's exact dialect and verifies that every declaration, comment
boundary, item kind and malformed statement survives. The returned edits refer
to the original snapshot and the output is already verified.

```rust
use adotenv::{parse_strict, FormatOptions};

let source = "# app   \n\nA=1   \nB =  2 # note   \n";
let doc = parse_strict(source).unwrap();
let formatted = doc.format(&FormatOptions::normalize()).unwrap();
assert_eq!(
    formatted.as_str(),
    "# app\n\nA = 1\nB = 2 # note\n"
);
assert!(parse_strict(formatted.as_str()).is_ok());
```

`EqualsSpacing::{Preserve, Spaces, None}`,
`BlankLines::{Preserve, Collapse}`, `QuoteStyle::{Preserve, Double, Single,
Unquoted}` and `FinalNewline::{Preserve, Ensure, Remove}` are explicit.
`FormatOptions::normalize_groups` also collapses runs of consecutive blank
lines to one group separator. Quote changes are accepted only when the exact
decoded value remains representable; empty assigned values keep their existing
quote style, and `Unquoted` is applied only with otherwise-preserved trivia in
this slice. Raw-value dialects, multiline value bodies,
invalid/ignored statements, empty assignments whose spacing could become
indentation, and mixed lone-CR boundaries are preserved. `FinalNewline::Remove`
removes one terminator from a nonblank final item; a trailing blank line is
structure and is kept. Group sorting and line-ending conversion are not part of
this slice.

## Sort within groups (0.5)

`Document::sort` orders declarations by key inside each blank-line-separated
group. Comments before the first entry stay as a group header; comments before
later entries move with them. Inline comments and multiline entries move intact.
`${VAR}` dependencies inside a group are placed before their dependents. Groups
with duplicates, empty keys, malformed/ignored items or dependency cycles are
left byte-for-byte unchanged.

```rust
use adotenv::parse_strict;

let source = "# header\nB=${A}\n# A note\nA=1\n# bottom\n";
let doc = parse_strict(source).unwrap();
let sorted = doc.sort().unwrap();
assert_eq!(
    sorted.as_str(),
    "# header\n# A note\nA=1\nB=${A}\n# bottom\n"
);
```

Sorting is separate from trivia formatting, returns the same verified-edit
shape, and performs no expansion, environment access or I/O.

## Compare files by key (0.5)

`Document::compare` reports effective keys that differ between two documents.
Missing keys, bare versus empty versus assigned values, decoded value changes and
export differences are distinct. Duplicate-key resolution and lookup
participation follow each document's own dialect.

```rust
use adotenv::{parse_strict, DifferenceKind};

let left = parse_strict("A=1\nB=\nC=3\n").unwrap();
let right = parse_strict("A=2\nB\nD=4\n").unwrap();
let comparison = left.compare(&right);
let changes: Vec<_> = comparison
    .differences()
    .iter()
    .map(|entry| (entry.key.as_str(), entry.kind))
    .collect();
assert_eq!(
    changes,
    [
        ("A", DifferenceKind::Value),
        ("B", DifferenceKind::State),
        ("C", DifferenceKind::OnlyLeft),
        ("D", DifferenceKind::OnlyRight),
    ]
);
```

Comparison is literal and side-effect-free. It does not expand values, read the
environment, execute commands or inspect files.

## Generate an example (0.5)

`Document::example` removes assigned values while retaining comments, blank
lines, keys, export prefixes, formatting, line endings and the BOM. Bare
declarations stay bare; an empty quoted placeholder keeps an inline comment
attached where the grammar requires a value body. Malformed/ignored statements
and entries with an empty key become indentation-preserving comments so they
cannot leak a value.

```rust
use adotenv::parse;

let source = "# app\nA=1\nB=\"two\" # note\nBARE\nBAD KEY=secret\n";
let (doc, _) = parse(source);
assert_eq!(doc.example(), "# app\nA=\nB= # note\nBARE\n#\n");
```

## Load files (0.6, opt-in)

Enable the `load` feature to read standard cascades from an explicit directory.
The order is `.env`, `.env.local`, `.env.{mode}`, `.env.{mode}.local`; later
files override earlier files by key. Use `load_cascade_values` when only final
strings are needed; use `LoadedEnvironment` when origins or contextual
resolution are required. Missing files are skipped, but a file that
exists and cannot be read, decoded or parsed invalidates the complete load.
Nothing is applied to the process environment.

```rust,ignore
use adotenv::load::load_cascade;

let loaded = load_cascade("/srv/my-app", "production")?;
assert_eq!(loaded.get("PORT"), Some("8080"));
let origin = loaded.origin("PORT").unwrap();
println!("{}:{}", origin.path.display(), origin.line);
for change in loaded.overrides() {
    println!("{}: {:?} -> {:?}", change.key, change.previous, change.replacement);
}
```

`entries()` retains every loaded declaration, including later-overridden ones;
`overrides()`, `repeated()` and `skipped()` explain precedence, duplicate keys
and bare/nonparticipating declarations. `load_files` accepts an explicit
ordered file list. Values remain literal; no expansion, environment lookup or
command execution occurs. Bare declarations are skipped unless the selected
dialect maps them to effective empty values.

The full loader retains values, origins, raw winning tokens and all reports by
default. When an application does not need contextual resolution or reports,
use `load_cascade_with_options` or `load_files_with_options` with `LoadOptions`:

```rust,ignore
use adotenv::load::{load_cascade_with_options, LoadOptions};

let options = LoadOptions::new(adotenv::Dialect::PORTABLE)
    .with_raw_tokens(false)
    .with_reports(false);
let loaded = load_cascade_with_options("/srv/my-app", "production", options)?;
assert_eq!(loaded.get("PORT"), Some("8080"));
assert!(!loaded.retains_raw_tokens());
```

Origins remain available, while `entries()`, `overrides()`, `repeated()` and
`skipped()` are empty when reports are disabled. Dropping raw tokens makes
`resolve_contextual` return an error; `resolve_decoded` still works. Use
`load_cascade_values` or `load_values` when origins are not needed either.

### Resolve loaded values

`LoadedEnvironment::resolve_contextual` is the preferred merged-environment
resolver. It reconstructs each winning declaration's raw key/value token and
reparses it with the load dialect, preserving quote, escape and dialect context.
`resolve_decoded` remains available for an explicit decoded-text policy.

```rust,ignore
use adotenv::load::load_cascade;
use adotenv::{ExpandOptions, ExpandScope};

let loaded = load_cascade("/srv/my-app", "production")?;
let resolved = loaded.resolve_contextual(
    |name| std::env::var(name).ok(),
    &ExpandOptions::portable().with_scope(ExpandScope::WholeFile),
)?;
assert!(resolved.get("PORT").is_some());
```

`resolve_decoded` is the explicit decoded-value policy: every final value is
treated as unquoted decoded text. Command substitutions remain literal in both
APIs.

## Apply to the process environment (0.7, opt-in)

The separate `adotenv-process` adapter applies a fully loaded environment
through an explicit `unsafe fn`. Keys and values are validated before the first
mutation; the adapter never clears or removes unrelated process variables and
cannot roll changes back. Returning a map from `LoadedEnvironment::values()` is
the default and does not mutate the process.

```rust,ignore
use adotenv::load::load_cascade;
use adotenv_process::apply;

let loaded = load_cascade("/srv/my-app", "production")?;
// The caller must guarantee no concurrent process-environment access.
unsafe { apply(&loaded)? };
```

The adapter is published as `adotenv-process` 0.9.7. See
[RELEASING.md](https://github.com/asamarts/adotenv/blob/main/RELEASING.md) for
the one-time crates.io bootstrap.

## Deserialize with serde (0.6, opt-in)

With the `serde` feature, `Document::deserialize` maps effective literal
declarations into a Serde type. Values are parsed as strings, booleans, numbers
or characters when requested; nested sequences/maps and enum layouts are not
invented. Duplicate keys follow the document dialect, bare declarations are
available only under `BareKeys::Empty`, and conversion errors carry the source
line and key.

```rust,ignore
use adotenv::parse_strict;
use serde::Deserialize;

#[derive(Deserialize)]
struct Config {
    host: String,
    port: u16,
}

let doc = parse_strict("host=localhost\nport=8080\n").unwrap();
let config: Config = doc.deserialize()?;
```

With `load` and `serde` enabled together, a loaded cascade can be deserialized
directly from its final merged values (0.8.0):

```rust,ignore
use adotenv::load::load_cascade;
let loaded = load_cascade("/srv/my-app", "production")?;
let config: Config = loaded.deserialize()?;
```

## CLI (0.9, workspace companion)

The zero-dependency `adotenv-cli` workspace binary accepts a global
`--dialect <portable|node|python|compose|docker>` option and currently provides:

```text
get <file> <key>          print an effective literal value
check [--format human|json|github] [--json] [--source]
      [--recursive] [--exclude <path>] <file-or-dir>...
                          print parse diagnostics
fmt [--write|--check] [--sort] [--recursive] [--exclude <path>] <file-or-dir>...
                          format, check or write documents
diff [--format human|matrix|json] [--json] <baseline> <file>...
                          compare effective keys
example <file>            print a value-free example
load <directory> [mode]   load the standard dotenv cascade
set <file> <key> <value>  set and write one declaration
run <directory> [mode] -- <program> [args...]
                          run a child with loaded values
```

`check` defaults to human output with path, one-based line and column,
severity, stable code, message and a summary. `--format json` (or the
compatibility alias `--json`) emits a versioned
`{"version":1,"files":[...]}` document with stable diagnostic codes,
severities, exact spans and read errors, suitable for editor integration.
`--format github` emits GitHub Actions workflow annotations instead. Human
output deliberately omits source lines because `.env` values may be secrets;
`--source` adds source lines and carets as an explicit opt-in and is only valid
for human output. The versioned JSON formats and GitHub annotation fields are
the stable machine contracts; human text and the matrix table are not. See
[the CLI contracts](docs/design/cli.md#machine-output-compatibility).

`check --recursive` and `fmt --recursive` discover `.env` and `.env.*` files
below the supplied directories (or `.` when no path is given). Repeatable
`--exclude <path>` values are relative to each root. Discovery skips `.git`,
`target`, `node_modules` and symbolic links, and does not follow them.

`diff` treats the first file as the baseline and compares every following file
to it. Two-file diff output is unchanged. `--format matrix` prints a key-by-file
table, while `--format json` (or `--json`) emits a versioned per-candidate
comparison document. The command exits 1 if any candidate differs.

File-reading commands accept `-` or `--stdin` for standard input. `run` loads
the cascade and starts a child with those values added to its environment; the
parent environment is never mutated. Child environment keys
and values are validated before execution. `fmt --check` reports required
changes without writing. On Unix, `--write` uses an atomic same-directory rename.
On Windows it writes a same-directory temporary file and attempts a direct
replace; if that fails, it moves the old file to a backup, installs the new file
and restores the backup on failure. This fallback is not crash-atomic. Command
substitution from files remains disabled. The CLI uses the library’s verified formatting, sorting,
loading and edit APIs. It is published as `adotenv-cli` 0.9.7; see
[RELEASING.md](https://github.com/asamarts/adotenv/blob/main/RELEASING.md).

## Integrations (0.9, workspace companions)

`adotenv-config` exposes effective literal declarations as a `config` source;
enable its `load` feature to accept a loaded environment. `config` applies its
native key rules: names are case-insensitive and dots denote nested paths.
`adotenv-figment` exposes a `Profile::Default` dictionary with the original key
spelling; enable its `load` feature for loaded environments. Both honor
duplicate policy, lookup participation and `BareKeys::Empty`.

```rust,ignore
use adotenv::parse_strict;
use adotenv_config::AdotenvSource;

let doc = parse_strict("host=localhost\nport=8080\n").unwrap();
let config = config::Config::builder()
    .add_source(AdotenvSource::from_document(&doc))
    .build()?;
```

`config` can deserialize typed structures using its own scalar coercion.
Figment values remain strings: deserialize string fields and convert explicitly.
Both adapters are published as 0.9.7 companion crates; see
[RELEASING.md](https://github.com/asamarts/adotenv/blob/main/RELEASING.md).

## Portable syntax (unchanged default)

- Keys are nonempty ASCII letters, digits, `_`, `.` and `-`. Non-POSIX names
  such as `app.key` or `1KEY` are accepted with a portability warning.
  Other characters, including non-ASCII letters, are invalid keys.
- An optional `export` prefix and spaces/tabs around `=` are accepted.
- Outside quotes, surrounding spaces/tabs are trivia. `A=a # comment` has
  value `a`; `A=a#b` has literal value `a#b` and a portability warning.
  `A= # comment` is an assigned empty value.
- Single quotes are fully literal. Double quotes decode `\n`, `\r`, `\t`,
  `\\`, `\"` and `\$`; unknown backslash pairs remain literal with a warning.
  Both quote styles can span physical lines, retaining original line endings.
- Non-comment text after a closing quote is an error, not silently discarded.
  An unmatched quote makes its opening physical line invalid; recovery resumes
  at the next line. A matching later quote follows the grammar, even across
  assignment-looking lines.
- `KEY` is a **bare declaration**: `value()` is `None`, unlike `KEY=` and
  `KEY=""`, whose values are `Some("")`. `AssignmentState` exposes the state.
  Bare declarations never consult the process environment.
- Duplicates remain ordered and generate warnings. `get_entry()` selects the
  last declaration; `get()` returns its value, including `None` if it is bare.
  Use `get_all()` to inspect every occurrence and `unique()` to reject ambiguity.
  `effective_entries()` yields the winning declaration and effective value for
  each key, including `""` for a bare key under `BareKeys::Empty`.
- LF, CRLF and lone CR line endings, a leading UTF-8 BOM and final-newline
  state are retained. BOM presence is informational; lone CR generates a warning.
- Backticks are literal, not quotes; unquoted backticks generate warnings.
  No form of expansion or command execution is part of parsing.

The input domain is UTF-8 (`&str` or `String`). Decoding bytes is the caller's
responsibility; silently replacing invalid UTF-8 would invalidate byte spans.

## Edit a snapshot

Key-only convenience methods require a unique declaration. Explicit native
selectors can target individual duplicate occurrences and remove comments or
invalid items without guessing their ownership:

```rust
use adotenv::{Edit, InsertPosition};
let doc = adotenv::parse_strict("A=one\nA=two\n").unwrap();
let second = doc.get_all("A").nth(1).unwrap();
let verified = doc.edit(&[
    Edit::Set { entry: second.id(), value: "changed" },
    Edit::Insert { key: "B", value: None, position: InsertPosition::End },
]).unwrap();
let (after, _) = adotenv::parse(verified.as_str());
assert_eq!(after.get("A"), Some("changed"));
assert_eq!(after.get_entry("B").unwrap().value(), None);
assert!(!verified.edits().is_empty());
```

`Document::set`, `remove`, `rename`, `insert`, `comment`, `uncomment` and
`set_export` produce `VerifiedEdits`, as does `Document::edit(&[Edit])`:

```rust
use adotenv::parse_strict;

let doc = parse_strict("A=1\nB=2\n").unwrap();
assert_eq!(doc.remove("A").unwrap().as_str(), "B=2\n");
assert_eq!(doc.rename("B", "C").unwrap().as_str(), "A=1\nC=2\n");
assert_eq!(doc.comment("A").unwrap().as_str(), "# A=1\nB=2\n");
assert_eq!(
    doc.set_export("B", true).unwrap().as_str(),
    "A=1\nexport B=2\n"
);

let commented = parse_strict("# A=1\n").unwrap();
let comment = commented.items().next().unwrap();
assert_eq!(commented.uncomment(comment.id()).unwrap().as_str(), "A=1\n");
```
Insertion supports end, after a unique key, after an explicit entry, and sorted
position (before the first original entry whose key is greater). All anchors
refer to the original snapshot. Comments are separate items: removal never
attaches them implicitly. Commenting prefixes each line under the selected
scanner's line-break policy; restoration can require several comment items,
including across explicitly removed intervening items. Empty selected EOF
comments can disappear. A selected uncomment may expose its own leading BOM;
exposing a BOM from unrelated source is refused.

`VerifiedEdits::edits()` borrows the native `TextEdit { range, replacement }`
list; `into_edits()` consumes it. `as_str()` borrows the verified output and
`into_string()` consumes it. Ranges refer to the **original** snapshot: an
editor must check the document version and apply non-overlapping ranges from
highest offset to lowest, not reinterpret them as sequential offsets.

Verification reparses the whole batch with the document's exact `Dialect` and
rejects conflicting operations, unrepresentable values, or changed interpretation
of unrelated syntax, including changed multiline quote grouping. Unrelated
invalid statements do not prevent editing a valid target. Existing quotes are
retained when exact; otherwise a safe candidate is verified. Python key quotes,
Node backticks, appended tails and raw Docker values retain their selected
grammar. Native set/insert use the same exact codec as standalone encoding,
with the actual header, suffix and line budget; they accept literal controls
when the selected grammar can represent them. Whole-batch preservation remains
mandatory even when a standalone value is encodable.

`encode_value_with(&str, Dialect) -> Result<String, EncodeError>` returns a
standalone literal token or `EncodeError::Unrepresentable`. Its contract is
`A=<token>\n` **and** `A=<token>` at EOF. The canonical `A=` header consumes two
bytes of a configured first-line budget. A Node token may begin with a line
break when a first-line cap cannot fit the opening quote; the grammar treats
that break as trivia between `=` and the value. This is not a context-safe
edit or an interpolation guarantee. Syntax padding is part of the returned
token: LF-only raw values ending in CR use an extra CR, while `LiteralFallback`
can require a trailing inline-comment marker. Do not append CRLF to raw padding;
use a native edit for an existing document.

```rust
use adotenv::{encode_value_with, parse_strict_with, Dialect};

let value = "literal #\0\n";
let token = encode_value_with(value, Dialect::PYTHON_DOTENV).unwrap();
let source = format!("A={token}\n");
assert_eq!(parse_strict_with(&source, Dialect::PYTHON_DOTENV).unwrap().get("A"),
           Some(value));
assert!(encode_value_with("two\nlines", Dialect::DOCKER_RAW).is_err());
```

`encode_value(&str) -> Option<String>` keeps its Portable default and its
published refusal of controls other than newline, carriage return and tab.

The free `adotenv::set(text, key, value)` and `remove(text, key)` helpers still
return `Result<String, EditError>`, but now use the recovering parser and native
verified producer. They are not a 0.1 grammar compatibility layer.

## Editor positions

```rust
use adotenv::{parse_strict, ColumnEncoding, Position};

let doc = parse_strict("A=é\n").unwrap();
let index = doc.line_index();
let position = index.to_position(2, ColumnEncoding::Scalar).unwrap();
assert_eq!(position, Position { line: 0, column: 2 });
assert_eq!(
    position.one_based().unwrap(),
    Position { line: 1, column: 3 }
);
assert_eq!(
    index
        .to_offset(Position { line: 0, column: 2 }, ColumnEncoding::Scalar)
        .unwrap(),
    2
);
```

`doc.line_index()` constructs an explicit index. `to_position(offset, encoding)`
and `to_offset(Position { line, column }, encoding)` are checked conversions;
`ColumnEncoding` is `Utf8` (bytes), `Scalar` or `Utf16` (code units). Positions
are zero-based; `Position::one_based()` explicitly performs checked display
conversion. `Entry::line()` and `Diagnostic::line()` remain one-based.

EOF is valid, and a terminal line ending creates a final empty line. Line-ending
starts map to the prior content end; the interior of CRLF is not representable.
Columns beyond line content and split UTF-16 surrogate pairs are rejected,
never rounded or clamped. Scalar/UTF-16 conversions reject interior UTF-8
bytes; UTF-8 columns preserve exact byte offsets, including those inside a
character. Text-edit boundaries must still be UTF-8 aligned. The BOM counts
in original-source coordinates. `Entry::decoded_offset_to_raw` and
`decoded_span_to_raw` map decoded value byte boundaries back to raw source
spans, including escapes.

## Migrating from 0.3

0.4 is additive. Parsing, editing and 0.3 dialect APIs keep their behavior.
`Entry::references`, `Document::resolve`, command callbacks and the `exec`
feature are opt-in; commands remain literal unless a callback is supplied and
the selected rules enable them. The `exec` feature is disabled by default and
is the only process-spawning path.

## Migrating from 0.7

`Dialect`, `FormatOptions`, `ExpandOptions` and `ReferenceRules` are
`#[non_exhaustive]` in 0.8. External struct literals no longer compile. Start
from a preset or `Default` and use builders or mutate public fields:

```rust
use adotenv::{BareKeys, Dialect, ExpandOptions, ExpandScope};

let dialect = Dialect::NODE_DOTENV.with_bare_keys(BareKeys::Ignore);
let options = ExpandOptions::portable().with_scope(ExpandScope::WholeFile);
```

Library-produced report/result structs are also non-exhaustive, as are the
dialect/format policy enums and `Multiline`. Simple caller-built values such as
`TextEdit` and `Position` remain constructible with struct literals. This
preserves the ability to add policy, diagnostic and enum variants after 1.0
without another breaking change.

## Migrating from 0.2

0.3 is additive for parsing and editing. Portable defaults are unchanged.
Code that constructs `EntryTokens` must supply the new `quoted_tail` field
(`None` when absent). `Dialect`, `parse_with`, `encode_value_with` and
`Document::portability` are new APIs; no preset enables expansion, inheritance
or loading. See the dialect section for the exact token span rules.

## Migrating from 0.1

0.2 is a breaking release, not an alternate legacy parser:

1. Replace `parse(text)?` / `parse(text).unwrap()` with `parse_strict(text)` for
   first-error handling, or destructure `let (doc, diagnostics) = parse(text)`.
2. Replace `doc.entries()[i]` with `doc.entry(i)` or iterator methods. Views are
   borrowed from the document; owned source uses `parse_owned` / `into_owned`.
3. Handle `Option` from `Entry::value()`, `raw_value()` and `value_span()` because
   bare declarations are retained, not rejected. A last bare duplicate does
   not fall back to an earlier assigned value.
4. Review ASCII key restrictions, whitespace-sensitive hash comments (including
   `A= # comment`), `\$` decoding and unknown-escape diagnostics. Rejecting
   quoted tails and accepting multiline quotes can change statement grouping.
5. Match `DiagnosticCode`, severity and spans instead of the old parse error
   type or message wording. Strict parsing still allows portability warnings.
6. Prefer document-native edits for selected occurrences and verified batches;
   their result is `VerifiedEdits`, not a string. Free `set` / `remove` retain
   string results but use the new Portable grammar and verification rules.
7. Update the toolchain to Rust **1.81**. The default `std` feature is optional:
   `adotenv = { version = "0.2", default-features = false }` uses `alloc` only.

## Migrating and adopting

See [MIGRATION.md](MIGRATION.md) for the 0.9-to-1.0 compatibility path, older
migration links and an adoption checklist. The live 1.0 evidence status is in
[1.0-readiness.md](docs/design/1.0-readiness.md).

## Quality evidence

Criterion workloads cover small, typical and multi-megabyte parsing, exact
per-dialect value encoding, portability reports, reference inspection,
resolution, structure/formatting/sorting and native edits including large
set/set-rename batches. Public synthetic seeds feed parser, edit, encoding,
resolution and format fuzz targets; scheduled CI runs bounded campaigns.
Reproduction commands and evidence policy are in
[quality.md](docs/design/quality.md); the current local baseline is in
[performance.md](docs/design/performance.md), including the ecosystem
comparison, memory profile and optimization backlog. Harness availability is
not a claim that any particular benchmark or fuzz duration has passed.

The public [upstream conformance corpus](conformance/manifest.json) records
version-pinned real Node, Python, Compose and Docker env-file observations.
Run `python3 conformance/check_adotenv.py --setup` on Linux to replay the real
APIs and compare applicable literal results with the Rust presets.
[Profile details and validation evidence](docs/design/quality.md#public-upstream-conformance-corpus-03-preparation)
separate parsing, resolution and loading. Compose comparisons exclude
dollar-containing inputs and supplied lookup environments; its real default API
does not provide an interpolation-off oracle. Portable is not asserted equivalent
to an upstream dialect.

The runner binds declared versions to locked artifacts and executed package
identities, validates each profile's observation protocol, and hashes exact input
snapshots before setup/execution. Changes to cases, profiles, manifest or locks
during a run abort comparison/recording rather than blessing mixed inputs.
Reviewed source commits remain distinct from distribution/module artifact pins.

Recovering `parse` / `parse_owned` retain items and all diagnostics. Strict
parsing and native edit verification omit discarded diagnostic lists and
duplicate-warning key indexes; primary item diagnostics remain available.
Callers handling untrusted input must still apply their own byte and
line/declaration budgets before parsing: retained documents, decoded escape
maps and requested diagnostics consume memory. Docker's preset includes its
upstream scanner line-byte limit; this is not an application memory ceiling.

0.3 includes explicit dialect configuration, five presets, per-dialect
encoding, order-independent same-line native set batches under byte caps and
cross-dialect portability reporting. 0.4 adds literal reference inspection,
opt-in variable resolution, explicit command callbacks and an opt-in `exec`
helper. 0.5 adds formatting, structure views, comparison, examples and sorting;
0.6 adds loading, serde and the process adapter; 0.7 adds the CLI and the
`config`/Figment integration companions. See [ROADMAP.md](ROADMAP.md) and
[design notes](docs/design/).

## Minimum supported Rust version

Rust 1.81 (edition 2021). Raising the MSRV is a minor-version change;
development and fuzz dependencies may require a newer toolchain.

## License

Licensed under either [Apache License, Version 2.0](LICENSE-APACHE) or
[MIT license](LICENSE-MIT), at your option.