<!-- entire-trail-link-start -->
https://entire.io/gh/jdx/mise/trails/614
<!-- entire-trail-link-end -->
Since 2026.10.3, mise reads a dotenv value as exactly one of
single-quoted, double-quoted, or unquoted. Before that, values were read
like a shell word: quoted and unquoted parts next to each other were
joined and their quotes removed. Common `.env` lines broke:
```sh
A='it'\''s'
B=a'b c'd
C="one"'two'three
```
| | before 2026.10.3 | 2026.10.3–2026.10.6 | this PR |
|---|---|---|---|
| `A` | `it's` | syntax error | `it's` |
| `B` | `ab cd` | `a'b c'd` (quotes kept) | `ab cd` |
| `C` | `onetwothree` | syntax error | `onetwothree` |
The syntax error hurt most under the `env_file` setting
(`MISE_ENV_FILE=.env`): mise warned and dropped every line after `A`. In
`_.file`, it failed the whole config.
This PR joins value segments again, as before 2026.10.3. These stay the
same:
- Single-quoted parts are fully literal: no escapes, no `$VAR`
substitution (#14228).
- Variables resolve against the file's own earlier assignments first.
- A `${...}` expression is one unit, so quotes in a default (`${NAME:-"a
b"}`) don't split the value, and a brace inside a quoted default
(`${NAME:-"{}"}`) doesn't end the expression.
### Compatibility notes
- Quotes inside an unquoted value are now removed again. A JSON value
that must keep its quotes needs single quotes: `CONFIG='{"debug":
true}'`. Before 2026.10.3, `CONFIG={"debug":true}` gave `{debug:true}`;
2026.10.3–2026.10.6 kept the quotes. Unquoted JSON with spaces was a
syntax error before 2026.10.3, so files written for older mise are
unaffected.
- A quote partway through a value opens a quoted part only when its
partner comes later on the same line. Otherwise it's a literal
character, so `NAME=O'Brien` and `GREETING=it's great` read as written,
as they did in 2026.10.3–2026.10.6 (dotenvy rejected both). Only a
value's opening quote may span lines, as before. Quotes and comments are
read left to right, so a partner inside a trailing comment still counts
(as in dotenvy, `a'x #y'` is `ax #y`): write `NAME="O'Brien" # owner's
name`.
- Text after a closing quote is now part of the value (`KEY="a"b` gives
`ab`), so `mise_dotenv::ParseErrorKind::TrailingCharacters` no longer
occurs and is removed.
### Validation
- New unit tests cover concatenation, the quote/comment interactions,
substitution across segments, and dotenvy 0.15.7's own
`test_parse_value_escapes` cases (e.g. `KEY4='sweet $\fgs'\''fds'` →
`sweet $\fgs'fds`). Existing tests that asserted literal quotes in
unquoted values now expect dotenvy's results.
- `render_value` now quotes any value containing `'` or `"`. Its
round-trip property tests pass with 20,000 cases.
- New e2e test `e2e/env/test_env_file_quote_concatenation` covers
`_.file` and `MISE_ENV_FILE`. It fails without this change with the old
syntax error.
- A differential fuzz against dotenvy 0.15.7 compared ~936k single-line
values that dotenvy accepts. With the divergences that predate this PR
filtered out, the only difference left is a dotenvy quirk that keeps one
space before a comment after `$VAR`.
- `cargo clippy --workspace --all-features --all-targets -- -D warnings`
passes, and `cargo +1.95 check -p mise-dotenv` passes.
Divergences from dotenvy that predate this PR and are unchanged: an
unquoted `\n` stays a literal backslash-n (dotenvy wrote a newline), a
trailing lone `\` or `$` is kept (dotenvy dropped it), and `\ ` inside
double quotes is kept as written.
This branch is stacked on #14228 and includes its commit. Merge #14228
first; this diff then shrinks to the concatenation change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
*AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5-5;
version: 2.1.293.*
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes how every dotenv file is parsed (including unquoted JSON and
post-quote text), which can alter loaded env values versus
2026.10.3–2026.10.6 even when fixing previously broken lines.
>
> **Overview**
> Restores **shell-style dotenv value parsing** in `mise-dotenv` after
2026.10.3 treated each value as only single-, double-, or unquoted.
Adjacent segments are joined again (`'it'\''s'` → `it's`, `a'b c'd` →
`ab cd`), fixing syntax errors that could drop or abort the rest of a
`.env` under `MISE_ENV_FILE` or `_.file`.
>
> Parsing now walks values with `parse_value`: quotes open a segment
only when a closing partner exists on the same line (so `O'Brien` stays
literal); single-quoted text stays fully literal; `${...}` defaults
treat quoted words as one unit. **`TrailingCharacters` is removed**—text
after a closing quote is part of the value. **`render_value`** treats
paired quotes like the parser so round-trips stay consistent.
>
> Docs and new unit/e2e tests document quoting, JSON via single quotes,
and `_.file` / `env_file` behavior.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
90d01d16de. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Dotenv values now combine adjacent quoted and unquoted segments,
removing quote delimiters while preserving single-quoted text literally.
* Quoted segments can be followed by additional value text. An unmatched
quote within a value is treated literally; unmatched opening quotes
still produce an error.
* Single-quoted values preserve backslashes, including in paths and
literal escape sequences.
* Values containing quote characters are now quoted when rendered,
preserving them when read again.
* **Documentation**
* Updated dotenv guidance to explain shell-style quoting, concatenated
values, and references to earlier assignments, with examples.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
mise-dotenv
A dotenv parser used by mise for .env files. Vendored and trimmed from
dotenv-ng-core 1.0.0 (itself a fork of
dotenvy), MIT licensed; see LICENSE.
Unlike upstream's loader, mise_dotenv::parse never reads the process environment, so a file's own
assignments are not shadowed by ambient variables. Process-environment mutation was removed.
Unlike upstream, a value is read like a shell word, as dotenvy read it: adjacent unquoted,
single-quoted, and double-quoted parts are joined with their quotes removed, so 'it'\''s' is
it's. Single-quoted parts are fully literal.