All notable changes to oneenv are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[1.10.2] - 2026-08-07
Added
- Row numbers, REQUIRED column, and terminal colors. Enhanced table output with line numbers, a dedicated
REQUIREDcolumn, and ANSI terminal color highlights.
Fixed
- Cross-platform file watcher event debouncing. Debounced file notification streams on Linux (
inotify), macOS (kqueue), and Windows (ReadDirectoryChangesW) to prevent duplicate reload triggers on atomic file saves. - Windows watcher cancellation deadlock. Called
CancelIoExprior to closing handles to unblock synchronousReadDirectoryChangesWcalls cleanly on context cancellation.
Improved
- CLI test coverage. Added comprehensive unit tests for CLI subcommands (
doctor,lint,format,init,explain,migrate), elevating statement coverage to 90.9%.
1.10.1 - 2026-08-06
Added
- Colored terminal table output. When printing to a terminal,
WithTable()andPrint()render header in bold cyan, values in green, and sources in color. - Red formatting for required empty fields. Required fields that are empty or unset are highlighted in bold red in terminal tables so missing configuration is immediately visible.
Fixed
WithRequired()and,requiredtag enforce non-empty values. Previously, a key that was present in a.envfile but set to an empty string passed required validation. Both forms now returnErrRequiredwhen a value resolves to"".WithStrictKeys()works withWithPrefixand nestedenvPrefix. Fixed a bug whereWithStrictKeys()reported valid.envkeys as unknown when a prefix option or structenvPrefixtag was used.
1.10.0 - 2026-08-06
Added
-
WithOnce()— the once-per-process load as an option, soLoadandParseget it without a different entry point:cfg, err := oneenv.Parse[Config](oneenv.WithOnce())The first call decodes, concurrent callers wait for it, and later ones are handed a copy of the result — error included — without touching the files.
Shared[T]andMustShared[T]share the same decode, so the two forms can be mixed.
1.9.2 - 2026-08-06
Fixed
- The table stays even with non-ASCII values. Column widths were counted in runes, so a CJK ideograph or an emoji — two columns wide in a terminal — and a combining accent — none at all — left the right border ragged. Widths are now measured the way a terminal draws them.
1.9.1 - 2026-08-06
Changed
- The table is ruled between every row, and the header and key column are bold where the output is a terminal. Redirected or captured output stays plain, so a table in a log file is still comparable.
1.9.0 - 2026-08-06
Added
Shared[T]/MustShared[T]— load the configuration once per process and hand the same value to every caller afterwards, so a config needed in six packages is not parsed six times. Concurrent first calls block until the one that won has finished; later calls return its result, error included, without touching the files.SharedLive[T]— the same, for aLive[T]that also follows the files.ResetShared[T]— drop what was remembered, for tests.
Fixed
- Repeated
Loads of the same target withWithWatchno longer accumulate watchers. Each target starts exactly one, whatever the number of calls, and the entry is forgotten once its context is cancelled so a laterLoadcan start again.
1.8.0 - 2026-08-06
Fixed
- A failed reload no longer leaves a half-updated configuration.
WithWatchdecoded straight into the target, so a reload that failed partway kept the fields it had already written and left the rest stale. Each reload now decodes into a fresh value and replaces the target only once it has fully succeeded.
Added
NewLive[T]— a configuration holder that owns its lock: it loads once, follows the files until the context is cancelled, and hands every goroutine a snapshot throughGet.Errreports the last reload failure. This is the form to reach for when the configuration is read concurrently.WithMutex(mu sync.Locker)— for a value you own, the lock oneenv takes while it swaps a reload in, so readers taking the same lock never race with it.- Ruled tables. The
WithTableandPrintoutput is drawn with box characters, so a long value no longer runs into the next column at a glance. WithWatch()takes no callback. Every reload is reported throughWithLogger— a debug record on success, a warning on failure — so a callback is needed only when the program has to act on a reload.WithWatch(fn)is unchanged.
1.7.0 - 2026-08-06
Added
- A report of what is not set — the keys a struct declares that no source
supplied.
WithTableprints a summary line under the table (2 not set: SENTRY_DSN, SMTP_HOST),WithLoggerlogs one warning per load, andReport.Missing()/Report.MissingKeys()expose the list. A key that fell back to itsdefaultcounts as supplied. This is the mirror image ofWithStrictKeys, which reports keys in a file that the struct does not know.
1.6.0 - 2026-08-06
Added
-
WithWatch(onReload)— hot reload as an option, without importing a second package:oneenv.Load(&cfg, oneenv.WithContext(ctx), oneenv.WithWatch(func(err error) { ... }), )Loadreturns after the first decode and keeps re-decoding into the same target in the background until theWithContextcontext is cancelled. A failed reload leaves the last good values in place, and watching never starts if the initial load fails. -
SetPollInterval/PollInterval— the modification-time cadence used where no native notifier exists.
Changed
- The notifier implementations moved to
internal/notify, shared byWithWatchand theoneenv/watchpackage.watch.Watchandwatch.PollIntervalare unchanged.
1.5.1 - 2026-08-06
Added
-
AsParseError,AsFieldError,AsUnknownKeyError,AsConstraintError— the generic form oferrors.As: each returns the typed error and abool, so it can be used in the same expression that finds it.if pe, ok := oneenv.AsParseError(err); ok { return fmt.Errorf("config %s:%d: %s", pe.File, pe.Line, pe.Msg) }Plain
errors.Aswith a declared variable keeps working unchanged.
1.5.0 - 2026-08-06
Changed
-
The generated example file has a new shape. Every key is written without a value — the file is a form to fill in — and what used to be three comment lines above each key is now one inline comment after it:
APP_ENV= # type: string, required, allowed: local|staging|prod APP_PORT= # type: int, default: 8080, http listen portA
defaultorexampletag is described in the comment instead of being assigned, and each struct gets its own section, separated by two blank lines and headed with the field path, so variables that belong together stay together. Applies to bothExample[T]andWithWriteExample.
1.4.3 - 2026-08-06
Added
TYPEandNULLcolumns in theWithTableoutput, and aNullfield onReportentries (also shown byExplainand logged byWithLogger).NULLmarks a key that resolved to nothing, whether unset or set to an empty value.
Changed
- An empty secret prints as
"", not****. Masking a blank password claimed a value that was not there; a set password and an unset one now look different in the table.
1.4.2 - 2026-08-06
Changed
WithSecretReveal(n)narrows its window instead of giving up. A value too short to give both endsncharacters previously fell back to a full mask; now at most half of it is shown, split evenly between the two ends — an 8-character secret withn=4shows two characters at each end. Values shorter than eight characters are still masked in full, as is everything underWithRedacted().
1.4.1 - 2026-08-06
Changed
WithWriteExample()takes no argument: the generated example file lands beside the first.envthe call reads (.env→.env.example, honoringWithBaseDir). Passing a path still works.
1.4.0 - 2026-08-06
Added
- Typo detection —
WithStrictKeys()turns a.envkey that no field consumes into an*UnknownKeyError(errors.IsmatchesErrUnknownKey), soPORRT=9999fails at startup instead of silently doing nothing. - Source tracing —
WithReport(&rep)captures the full resolution detail of a load.rep.Source("PORT")names the file a value came from (.env.production) or the layer (env,default,unset),rep.Explainrenders everything known about one key, andrep.Entries()exposes the rest. Diff/DiffString— report every key whose value differs between two configurations, with secrets masked on both sides but still reported as changed.Hash/HashFull— a short, stable fingerprint of the effective configuration, for printing on deploy.WithBaseDir(dir)— resolve relative.envpaths against a directory.WithWriteExample([path])— regenerate an example file from the struct on every successful load, rewriting it only when its contents change. With no argument it sits next to the.envit documents (.env→.env.example, honoringWithBaseDir).WithOutput(w)— the single place a writer is named; everything oneenv prints goes toos.Stdoutotherwise.WithRedacted()— mask every secret in full, overridingWithSecretRevealwhatever the option order.- New struct tags —
example(sample value for.env.example),alias(former key spellings, with a deprecation warning when used),deprecated(warn whenever the key is used),enum(allowed values,ErrNotAllowed) andpattern(regexp,ErrPattern). Each has anenv-*spelling too. - CLI subcommands —
oneenv doctor,lint,format [-w],explain KEY,initandmigrate [-w] OLD=NEW.
Changed
- Secrets are now masked in full by default in the table, logger and
Printoutput.WithSecretReveal(n)is the only way to reveal any part of one; it previously defaulted to revealing four characters at each end. - The
SOURCEcolumn names the file a value came from, e.g..env.local, instead of the genericfile. WithTable()andWithLogger()no longer require an argument — the table goes to stdout and the logger defaults toslog.Default().WithLogger(l)still accepts a specific logger.Printtakes the value first and no writer:oneenv.Print(cfg). Pair it withWithOutput(w)to capture the output. This is a breaking change to thePrintandWithTablesignatures introduced hours earlier in 1.3.0.
1.3.0 - 2026-08-06
Added
WithTable(w)— prints an alignedKEY / VALUE / SOURCEtable towonce aLoadsucceeds, one row per resolved field.SOURCEnames the layer that won:env,file,defaultorunset. Nothing is printed when the load fails.WithLogger(l *slog.Logger)— logs every file read and every field resolved at debug level, with the env key, struct field path, source and value.WithSecretReveal(n)— how many leading and trailing characters of a secret stay visible in theWithTableandWithLoggeroutput (default4,0masks everything). A value too short to reveal both ends without overlapping is masked completely, so a short secret never leaks in full.Print(w, v, opts...)— renders an already-decoded config as aKEY / VALUEtable with secrets masked, for use outside aLoad.Sourcetype with theSourceEnv,SourceFile,SourceDefaultandSourceUnsetconstants.
Load remains silent unless one of these options is passed, and the extra
lookup the source column needs is skipped entirely when it is not. Redacted
and RedactedMap are unchanged: they still mask a secret in full.
1.2.0 - 2026-08-05
Fixed
- Secrets are no longer corrupted by
WithExpand(). Expansion treated every$in a value as a variable reference, so a password from a password manager was silently rewritten:pa$$worddecoded aspa$word,pa$wordaspa, and$ecret123as"".${PATH}inside a DSN expanded to the realPATH. Fields holding secrets now decode the literal text from the file:Secret[T]fields, fields tagged,secret/env-secret:"true", and fields tagged,noexpand. Non-secret fields expand exactly as before.
Added
,noexpandtag (andenv-noexpand:"true") — opt a non-secret field out of${VAR}/$VARexpansion.WithExpandStrict()— expansion where a reference to a variable defined neither earlier in the file nor in the process environment fails with a*ParseErrorwrapping the newErrUnknownVariable, instead of expanding to the empty string.ErrUnknownVariablesentinel, andParseError.Errwith anUnwrapmethod, so parse failures can be matched witherrors.Is.
1.1.0 - 2026-07-19
Added
Example[T]— generates a ready-to-fill.env.examplefrom a struct: each key with its default, preceded by comments carrying thedesctag, the Go type, and whether it is required. Defaults ofsecretfields are never written.- CLI
-exampleflag —oneenv -examplewrites a.env.examplefrom the merged.envfiles: keys kept, values stripped, original comments above each key preserved, plus inferred-type and required comments.-osets the output path (-for stdout, default.env.example).
1.0.0 - 2026-07-18
Initial release: a zero-dependency .env parser and struct decoder in one
package.
Added
- Core API —
Parse,Load,Unmarshal,LoadContext,ParseContext, and godotenv-styleRead,LoadEnv,Overload. - Types — ints/uints/floats,
bool,time.Duration,time.Time(withlayout), slices, maps, pointers, nested structs, and anyencoding.TextUnmarshaler. - Options — functional options:
WithFiles,WithEnvFiles,WithEnvVar,WithPrefix,WithOverride,WithExpand,WithRequired,WithTagKey,WithLookuper,WithTypeParser,WithMutator,WithValidator,WithContext. - Tags —
env(withrequired,notEmpty,file,init,unset,secretoptions),default,separator/envSeparator,layout,envPrefix,desc, plus anenv-*alias for every configuration tag (env-default,env-separator,env-description,env-layout,env-prefix,env-required,env-notempty,env-file,env-init,env-unset,env-secret). When both spellings are present theenv-*form takes priority. - Environment-aware file cascade —
WithEnvFiles()layers.env,.env.local,.env.<env>and.env.<env>.localby the active environment (APP_ENV, thenGO_ENV; configurable withWithEnvVar).FilesForexposes the resolved list. - Secrets —
env:"X,file"reads a value from the file at the resolved path;env:"X,secret"withRedacted/RedactedMapmasks values in output, and theSecret[T]wrapper keeps sensitive values out of logs and JSON while exposing the real value viaValue(). - Slices of structs — a
[]Structfield is decoded from indexed keys (SERVER_0_HOST,SERVER_1_HOST, …). - Hot reload — the
oneenv/watchsubpackage re-decodes on file change via native OS notifications (inotify on Linux, kqueue on BSD/macOS, ReadDirectoryChangesW on Windows) with modification-time polling as a fallback — all standard library, zero dependencies. - Marshal & Usage —
Marshal/MarshalMaprender a struct back to.env;Usageprints a--helptable of the variables a struct consumes. - Errors — positioned
*ParseError(file:line) and every field failure collected at once viaerrors.Join; sentinelsErrNotAStruct,ErrRequired,ErrEmpty,ErrSecretFile,ErrUnsupportedType. - Hermetic testing — a
Lookuperinterface (MapLookuper,OSLookuper,PrefixLookuper) with no global state, parallel-safe by design. - Runnable examples for the full API surface, so pkg.go.dev renders interactive examples.