dotenv

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: MIT Imports: 9 Imported by: 0

README

go-rotini/dotenv

A Go .env file parser: a conservative, portable subset of the .env dialects in the wild, with opt-in extensions, a lossless AST for round-trip editing, a linter that flags non-portable constructs, and a Source that drops straight into an environment-variable loader.

This package is the .env parsing layer for go-rotini/env and the rotini CLI framework — but it has no dependencies: nothing outside the standard library, and no other go-rotini module either. dotenv.Source is a small struct whose Lookup method is structurally compatible with go-rotini/env's Source interface, so a parsed .env file plugs into an env loader's source chain without dotenv importing env; likewise WithExpansionParent takes a one-method VarLookup interface that any env.Source (e.g. env.OSEnv()) satisfies.

Watching / live reload is not in this package. Re-reading a .env file when it changes on disk is handled one layer up, by a package that composes go-rotini/fs's file watcher with dotenv.Parse — the same mechanism used for yaml/toml/jsonc/jsonschema config files. dotenv itself reads the file once at construction; subsequent on-disk changes do not propagate.

Features

  • Conservative portable .env subset by default; every dialect-specific extension (export prefix, YAML-style KEY: value, backtick quoting, variable expansion, multi-line double-quoted strings, relaxed identifiers) is opt-in via ParseOptions.
  • Bash-style variable expansion (opt-in): $VAR, ${VAR}, ${VAR:-default}, ${VAR-default}, ${VAR:?error}, \$ to suppress; resolve references not defined in the file from the process environment with WithExpandFromOSEnv(), or from any VarLookup (e.g. env.OSEnv(), env.Map(...)) via WithExpansionParent.
  • Lossless File AST: entry order and per-entry source bytes are preserved, so (*File).Marshal is byte-exact on unmutated input; Set/Delete re-render only the affected line. (Set panics on a syntactically invalid key — see IsValidKey. Variable expansion done at parse time is not reflected by Marshal; use ToMap.)
  • Write back out: Marshal(map[string]string) ([]byte, error) renders a map as .env bytes (sorted keys, minimal quoting); Write(path, map) does that plus a 0o600 write.
  • Lint reports deviations from the portable subset (export prefix, KEY: value, non-POSIX identifiers, backtick values) as warnings, and parse failures as errors.
  • Source adapters: NewSource (path), NewReaderSource (io.Reader), MapSource (map), and (*File).Source — all return a *Source that any env loader accepts as a Source.
  • DoS guards: max file size (default 10 MiB), max line length (default 1 MiB), and a bounded expansion-recursion depth (default 16) that catches deeply nested / self-referential ${VAR:-default} chains.
  • Zero dependencies — go.mod has no require line at all (only the dev tool block).

Installation

go get github.com/go-rotini/dotenv

Requires Go 1.26 or later.

Quick start

Parse to a map
import "github.com/go-rotini/dotenv"

m, err := dotenv.Load(".env", dotenv.WithExpand())
if err != nil { log.Fatal(err) }
fmt.Println(m["DATABASE_URL"])

dotenv.Parse([]byte, ...ParseOption) does the same from a byte slice and returns the *File AST.

Use as a loader source

dotenv.NewSource returns a *dotenv.Source, which satisfies go-rotini/env's Source interface structurally (no import edge needed):

import (
	"github.com/go-rotini/env"
	"github.com/go-rotini/dotenv"
)

src, err := dotenv.NewSource(".env", dotenv.WithExpandFromOSEnv())
if err != nil { log.Fatal(err) }

// OSEnv() listed first wins over the .env file (the standard Compose / Heroku
// precedence model). Reverse the order to make the file authoritative.
loader := env.New(env.WithSource(env.OSEnv(), src))

var cfg Config
if err := loader.Load(&cfg); err != nil {
	log.Fatal(env.FormatError(err))
}

dotenv.NewReaderSource(io.Reader, ...ParseOption) builds a *Source from an already-open stream; dotenv.MapSource(map[string]string) from an in-memory map (handy as a WithExpansionParent). All take a snapshot at construction. *Source has Lookup, Keys, and ToMap. If you already have a *File (from Parse) and also want a loader source, call f.Source() — no re-parsing.

Lossless editing
data, _ := os.ReadFile(".env")
f, err := dotenv.Parse(data)
if err != nil { log.Fatal(err) }

f.Set("FEATURE_FLAGS", "feature-a,feature-b") // re-renders only this line
f.Delete("OBSOLETE_KEY")

out, _ := f.Marshal() // byte-exact for every untouched line
os.WriteFile(".env", out, 0o600)

Set panics if the key isn't a valid .env key (dotenv.IsValidKey is the predicate; the panic value wraps dotenv.ErrInvalidKey). It updates the first assignment of the key in place — preserving that line's original line ending — and removes any later assignments of the same key, so Get/ToMap agree with the value just set; if the key is absent it appends a new line (matching the source's line ending, and terminating the previous last line first if the source lacked a trailing newline). (*File).Marshal reflects Set/Delete mutations, not variable expansion performed at parse time (use ToMap/Get for expanded values).

Write a .env file from a map
m := map[string]string{"PORT": "8080", "GREETING": "hello world"}

b, err := dotenv.Marshal(m)            // []byte: sorted keys, LF endings, minimal quoting
if err != nil { log.Fatal(err) }       // err (wrapping ErrInvalidKey) if a key is invalid
_ = b

if err := dotenv.Write(".env", m); err != nil { // Marshal + write with 0o600 (not atomic)
	log.Fatal(err)
}
Lint
issues, err := dotenv.Lint(data)
for _, iss := range issues {
	fmt.Printf("%s: [%s] %s: %s\n", iss.Pos, iss.Severity, iss.Rule, iss.Message)
}
// err != nil when the input failed to parse; warnings collected before the
// failure are still returned.

Parse options

Option Effect
WithExpand() Enable $VAR / ${VAR...} expansion in unquoted and double-quoted values.
WithExpandFromOSEnv() Like WithExpand(), and resolve references not defined in the file from the process environment (os.LookupEnv).
WithExpansionParent(VarLookup) Resolve references not defined in the file from a VarLookup (e.g. env.OSEnv(), env.Map(...)). Requires WithExpand() or WithExpandFromOSEnv().
WithStrictExpansion() Unset variable references raise *ExpansionError (cause ErrUnsetVariable) instead of expanding to empty.
WithMaxExpansionDepth(int) Bound expansion recursion (default 16).
WithBackticks() Accept backtick-quoted values (motdotla/dotenv dialect); treated like single quotes.
WithStrictDotenv() Reject non-portable constructs (KEY: value, export prefix, non-POSIX identifiers).
WithRelaxedNames() Accept identifiers with hyphens, dots, or non-ASCII characters.
WithMaxFileSize(int64) Cap total input size (default 10 MiB; ≤0 = unlimited).
WithMaxLineLength(int) Cap a single physical line (default 1 MiB; ≤0 = unlimited).

Errors

  • *ParseError (errors.Is(err, dotenv.ErrParse)) — malformed .env source; carries file path and line/column (ParseError.Pos, a dotenv.Position).
  • *ExpansionError (errors.Is(err, dotenv.ErrExpansion)) — variable-expansion failure. Its Cause is dotenv.ErrUnsetVariable for an unset reference under strict expansion, dotenv.ErrAssertionFailed for a failed ${VAR:?msg} assertion, or dotenv.ErrMaxExpansionDepth when the WithMaxExpansionDepth bound was exceeded.
  • dotenv.ErrFileTooLarge, dotenv.ErrLineTooLong — resource-limit violations.
  • dotenv.ErrInvalidKey — wrapped by the error Marshal/Write return (and by the value (*File).Set panics with) when a key isn't a valid .env identifier.

Documentation

Full API reference is available on pkg.go.dev.

Contributing

See CONTRIBUTING.md for guidelines on how to contribute to this project.

Code of Conduct

This project follows a code of conduct to ensure a welcoming community. See CODE_OF_CONDUCT.md.

Security

To report a vulnerability, see SECURITY.md.

License

This project is licensed under the MIT License. See LICENSE for details.

Documentation

Overview

Package dotenv parses `.env` files into a lossless AST, a key/value map, or a *Source suitable for an environment-variable loader.

The package targets a conservative portable subset of the `.env` dialects in the wild. Every dialect-specific extension (the `export ` prefix, YAML-style `KEY: value`, backtick quoting, variable expansion, multi-line double-quoted strings) is supported but gated behind explicit ParseOptions or, for non-portable constructs that nonetheless parse, surfaced as LintIssue SeverityWarning entries from Lint.

API summary

Dependencies

dotenv has no dependencies — not the standard library only in spirit, but literally: nothing outside the standard library, and no other go-rotini module either. Its Source is a struct whose single Lookup method is structurally compatible with the Source interface in github.com/go-rotini/env, so a parsed `.env` file drops straight into an env loader's source chain without dotenv importing env. Likewise WithExpansionParent takes the small VarLookup interface, which any env.Source (e.g. env.OSEnv()) satisfies; for the common case there is also WithExpandFromOSEnv, which uses only os.LookupEnv.

File watching

This package does not watch files. It reads the source once at construction (NewSource / NewReaderSource / Load); subsequent on-disk changes do not propagate. Live reload of a `.env` file is the job of a higher-level package that composes github.com/go-rotini/fs's file watcher with Parse — the same arrangement used for yaml/toml/jsonc/jsonschema config files — so the watching dependency lives in exactly one place.

Example

Parse a `.env` byte stream and read values out of the resulting AST.

package main

import (
	"fmt"
	"log"

	"github.com/go-rotini/dotenv"
)

func main() {
	f, err := dotenv.Parse([]byte("# app config\nAPP_NAME=demo\nPORT=8080\n"))
	if err != nil {
		log.Fatal(err)
	}
	for _, k := range []string{"APP_NAME", "PORT"} {
		v, _ := f.Get(k)
		fmt.Printf("%s=%s\n", k, v)
	}
}
Output:
APP_NAME=demo
PORT=8080

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrParse matches any [*ParseError].
	ErrParse = &ParseError{}

	// ErrExpansion matches any [*ExpansionError].
	ErrExpansion = &ExpansionError{}

	// ErrFileTooLarge is returned when an input exceeds [WithMaxFileSize].
	ErrFileTooLarge = errors.New("dotenv: file size exceeds limit")

	// ErrLineTooLong is returned when a single line exceeds [WithMaxLineLength].
	ErrLineTooLong = errors.New("dotenv: line length exceeds limit")

	// ErrUnsetVariable is the [ExpansionError.Cause] when expansion under
	// [WithStrictExpansion] references a variable that is not set.
	ErrUnsetVariable = errors.New("dotenv: variable referenced in expansion is unset")

	// ErrAssertionFailed is the [ExpansionError.Cause] when a `${VAR:?msg}`
	// assertion fails during expansion.
	ErrAssertionFailed = errors.New("dotenv: ${VAR:?...} assertion failed")

	// ErrMaxExpansionDepth is the [ExpansionError.Cause] when expansion
	// recursion exceeds the [WithMaxExpansionDepth] bound.
	ErrMaxExpansionDepth = errors.New("dotenv: variable-expansion depth limit exceeded")

	// ErrInvalidKey reports a key that is not a valid `.env` identifier (see
	// [IsValidKey]). [Marshal] / [Write] return an error wrapping it; [File.Set]
	// panics with an error wrapping it.
	ErrInvalidKey = errors.New("dotenv: invalid key")
)

Sentinel errors. The struct-pointer sentinels match by type via the corresponding Is method; the others match by value identity.

Functions

func IsValidKey

func IsValidKey(key string) bool

IsValidKey reports whether key is a syntactically valid `.env` key — one that round-trips through Parse when WithRelaxedNames is in effect. A valid key is non-empty, does not begin with a digit, and otherwise contains only identifier bytes: ASCII letters, digits and '_', plus — beyond the portable POSIX subset [A-Za-z_][A-Za-z0-9_]* — '-', '.', and non-ASCII bytes. (Keys outside the POSIX subset only re-parse under WithRelaxedNames; the POSIX subset is always safe.)

Marshal / Write reject an invalid key with an error wrapping ErrInvalidKey; File.Set panics with one.

func Load

func Load(path string, opts ...ParseOption) (map[string]string, error)

Load reads a `.env` file from path and returns its key/value map (last-write-wins per Bash convention).

func Marshal

func Marshal(m map[string]string) ([]byte, error)

Marshal renders m as a `.env`-format byte stream: one `KEY=VALUE` line per entry, keys in sorted (deterministic) order, LF line endings, values double-quoted only when a bare value would not round-trip. The output re-parses to an equivalent map via Parse / Load.

Marshal returns an error wrapping ErrInvalidKey if any key is not a valid `.env` key (see IsValidKey) — emitting such a key would produce output that fails to re-parse. To turn a parsed *File back into bytes (preserving comments and formatting), use [(*File).Marshal] instead.

Example

Marshal renders a map as `.env` bytes (sorted keys, LF endings, minimal quoting). The result re-parses via Parse / Load.

package main

import (
	"fmt"
	"log"

	"github.com/go-rotini/dotenv"
)

func main() {
	out, err := dotenv.Marshal(map[string]string{
		"PORT":          "8080",
		"DATABASE_URL":  "postgres://localhost/app",
		"FEATURE_FLAGS": "a,b,c",
		"GREETING":      "hello world", // forces quoting
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(out))
}
Output:
DATABASE_URL=postgres://localhost/app
FEATURE_FLAGS=a,b,c
GREETING="hello world"
PORT=8080

func Write

func Write(path string, m map[string]string) error

Write renders m with Marshal and writes it to path with 0o600 permissions (creating the file, or truncating an existing one without changing its mode). Write is not atomic; for crash-safe replacement, use Marshal and write the bytes through your preferred mechanism (for example, github.com/go-rotini/fs's atomic WriteFile).

Example

Write renders a map with Marshal and writes it to a file (0o600).

package main

import (
	"fmt"
	"log"
	"os"
	"path/filepath"

	"github.com/go-rotini/dotenv"
)

func main() {
	dir, err := os.MkdirTemp("", "dotenv-example")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(dir)

	path := filepath.Join(dir, ".env")
	if err := dotenv.Write(path, map[string]string{"FOO": "bar", "BAZ": "qux"}); err != nil {
		log.Fatal(err)
	}
	data, err := os.ReadFile(path)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(data))
}
Output:
BAZ=qux
FOO=bar

Types

type Entry

type Entry struct {
	Kind EntryKind

	// Key is the assignment key (without the `export ` prefix), empty
	// for non-assignment entries.
	Key string

	// Value is the decoded value: escapes interpreted, quoting
	// stripped, and (with [WithExpand]) variable references resolved.
	// Empty for non-assignment entries.
	Value string

	// Pos is the source position of the entry's first byte.
	Pos Position
	// contains filtered or unexported fields
}

Entry is one logical line in a parsed `.env` file. Most callers use File.Get / File.Set / File.Delete / File.Keys instead of reading the entry list directly.

type EntryKind

type EntryKind int

EntryKind classifies one logical line in a `.env` file.

const (
	// EntryBlank is a whitespace-only line.
	EntryBlank EntryKind = iota

	// EntryComment is a line whose first non-whitespace character is
	// `#`. Trailing comments on assignment lines are stored on the
	// assignment, not as separate EntryComment entries.
	EntryComment

	// EntryAssignment is a `KEY=VALUE` (or `KEY: VALUE`) line.
	EntryAssignment
)

type ExpansionError

type ExpansionError struct {
	Pos      Position
	Variable string
	Reason   string
	Cause    error
}

ExpansionError is returned when variable expansion fails: an unset variable under WithStrictExpansion, a `${VAR:?msg}` assertion failure, or the WithMaxExpansionDepth bound being exceeded by a deeply nested or self-referential `${VAR:-default}` / `${VAR:?msg}` construct. ExpansionError.Cause (and hence errors.Is) is ErrUnsetVariable, ErrAssertionFailed, or ErrMaxExpansionDepth respectively; every ExpansionError also matches ErrExpansion.

func (*ExpansionError) Error

func (e *ExpansionError) Error() string

func (*ExpansionError) Is

func (*ExpansionError) Is(target error) bool

func (*ExpansionError) Unwrap

func (e *ExpansionError) Unwrap() error

type File

type File struct {
	// contains filtered or unexported fields
}

File is the AST for a parsed `.env` source. Entry order and per- entry source bytes are preserved so File.Marshal is byte-exact on unmutated inputs.

A File returned by Parse / Load / NewSource is safe for concurrent reads (File.Get, File.Keys, File.ToMap, File.Entries, File.Marshal), but the mutating methods (File.Set, File.Delete) are not — guard a File you intend to mutate from concurrent goroutines.

func Parse

func Parse(data []byte, opts ...ParseOption) (*File, error)

Parse parses data as a `.env` byte stream and returns the resulting AST. Use File.Get / File.Keys / File.ToMap for access, or File.Marshal for round-trip emission.

func (*File) Delete

func (f *File) Delete(key string) bool

Delete removes every assignment for key. Returns true when at least one entry was removed. Comments and blank lines are unaffected.

func (*File) Entries

func (f *File) Entries() []Entry

Entries returns the entry list. The slice is not for caller mutation; use File.Set / File.Delete.

func (*File) Get

func (f *File) Get(key string) (value string, ok bool)

Get returns the value associated with key. When the key appears in multiple assignments the LAST occurrence wins (matching `source <file>` in Bash).

func (*File) Keys

func (f *File) Keys() []string

Keys returns assignment keys in insertion order, deduplicated to the first occurrence.

func (*File) Marshal

func (f *File) Marshal() ([]byte, error)

Marshal returns the file's source representation. For unmutated inputs the result is byte-exact with the original; entries mutated or added via File.Set are freshly rendered with default quoting while surrounding entries retain their original bytes.

Variable expansion performed at parse time (via WithExpand / WithExpandFromOSEnv) is NOT reflected here — Marshal emits the original source text; only File.Set re-renders a line. Use File.ToMap / File.Get for the expanded values.

func (*File) Path

func (f *File) Path() string

Path returns the file path the AST was loaded from, or "" for an in-memory Parse.

func (*File) Set

func (f *File) Set(key, value string)

Set assigns value to key. If key already appears, its first assignment is updated in place (re-rendered with sensible-default quoting, keeping that line's original line ending) and any later assignments of the same key are removed — so the file holds a single authoritative assignment and File.Get / File.ToMap agree with the value just set. If key is absent, a new assignment is appended on its own line, matching the line ending the source's last line uses. Surrounding entries keep their original bytes, so round-trip is byte-exact except for the mutated/added line.

Set panics (with an error wrapping ErrInvalidKey) if key is not a valid `.env` key — see IsValidKey.

Example

Edit a `.env` file losslessly: Set updates an existing assignment in place (or appends a new one), Delete removes one, and Marshal emits the result — comments, quoting and untouched lines are preserved byte-for-byte.

package main

import (
	"fmt"
	"log"

	"github.com/go-rotini/dotenv"
)

func main() {
	f, err := dotenv.Parse([]byte("# generated config\nDB_HOST=localhost\nDB_PORT=5432\nDEBUG=true\n"))
	if err != nil {
		log.Fatal(err)
	}
	f.Set("DB_PORT", "5433") // update in place
	f.Set("DB_NAME", "app")  // append
	f.Delete("DEBUG")
	out, _ := f.Marshal()
	fmt.Print(string(out))
}
Output:
# generated config
DB_HOST=localhost
DB_PORT=5433
DB_NAME=app

func (*File) Source

func (f *File) Source() *Source

Source returns a *Source over the file's effective key/value mapping (File.ToMap) — a snapshot independent of the AST, so later File.Set / File.Delete calls do not change it. Use this when you parsed for editing but also want a value to hand to an environment-variable loader without re-parsing.

func (*File) ToMap

func (f *File) ToMap() map[string]string

ToMap returns the file's effective key/value mapping (last-write- wins per Bash convention).

type LintIssue

type LintIssue struct {
	Pos      Position
	Rule     string
	Message  string
	Severity Severity
}

LintIssue describes a deviation from the portable `.env` subset: `export ` prefix, YAML-style `KEY: value`, non-POSIX identifiers (hyphens, dots), or backtick-quoted values.

func Lint

func Lint(data []byte, opts ...ParseOption) ([]LintIssue, error)

Lint reports constructs in data that fall outside the portable `.env` subset. The returned slice may contain warnings even when err is non-nil — warnings encountered before a parse failure are preserved.

Warnings (SeverityWarning entries) come from a fast surface scan, not from the real parser, so on malformed input they can be approximate or diverge from what Parse reports; the returned error is authoritative for "does this parse". When parsing fails, the failure is reported both as a SeverityError entry (carrying the failure Position) and as the returned error.

Example

Lint flags constructs outside the portable `.env` subset (warnings) and any parse failure (an error). The input below parses, but uses two non-portable forms.

package main

import (
	"fmt"

	"github.com/go-rotini/dotenv"
)

func main() {
	data := []byte("export DATABASE_URL=postgres://localhost/app\nPORT: 8080\n")
	issues, err := dotenv.Lint(data)
	fmt.Println("err:", err)
	for _, iss := range issues {
		fmt.Printf("line %d: %s %s\n", iss.Pos.Line, iss.Severity, iss.Rule)
	}
}
Output:
err: <nil>
line 1: warning export-prefix
line 2: warning yaml-separator

type ParseError

type ParseError struct {
	Pos     Position
	Message string
}

ParseError is returned by Parse / Load / NewSource when the `.env` source is malformed. It carries the file path (when available) and the line/column position of the failure.

func (*ParseError) Error

func (e *ParseError) Error() string

func (*ParseError) Is

func (*ParseError) Is(target error) bool

type ParseOption

type ParseOption func(*parseOptions)

ParseOption configures Parse, Load, NewSource, NewReaderSource, and Lint.

func WithBackticks

func WithBackticks() ParseOption

WithBackticks accepts backtick-quoted values (motdotla/dotenv dialect). Treated like single-quoted: literal, no escapes, no expansion. Default is to reject as a syntax error.

func WithExpand

func WithExpand() ParseOption

WithExpand enables Bash-style variable expansion in unquoted and double-quoted values: `$VAR`, `${VAR}`, `${VAR:-default}`, `${VAR-default}`, `${VAR:?error}`. Backslash-dollar (`\$`) suppresses expansion. Lookups walk the file's already-parsed entries first, then any parent set via WithExpansionParent.

Example

WithExpand turns on Bash-style variable expansion. References resolve against the file's already-parsed entries first; `${VAR:-default}` supplies a fallback; nested references are expanded recursively.

package main

import (
	"fmt"
	"log"

	"github.com/go-rotini/dotenv"
)

func main() {
	src := "DOMAIN=example.com\n" +
		"API_URL=https://api.${DOMAIN}\n" +
		"ADMIN_EMAIL=${ADMIN_EMAIL:-admin@${DOMAIN}}\n"
	f, err := dotenv.Parse([]byte(src), dotenv.WithExpand())
	if err != nil {
		log.Fatal(err)
	}
	for _, k := range []string{"API_URL", "ADMIN_EMAIL"} {
		v, _ := f.Get(k)
		fmt.Println(k, "=", v)
	}
}
Output:
API_URL = https://api.example.com
ADMIN_EMAIL = admin@example.com

func WithExpandFromOSEnv

func WithExpandFromOSEnv() ParseOption

WithExpandFromOSEnv enables variable expansion (like WithExpand) and resolves references not defined in the file against the process environment via os.LookupEnv. It is equivalent to `WithExpand()` plus `WithExpansionParent(<an os.LookupEnv-backed VarLookup>)`, using only the standard library.

func WithExpansionParent

func WithExpansionParent(parent VarLookup) ParseOption

WithExpansionParent sets the VarLookup consulted by variable expansion when a referenced variable is not present in the file's own entries. It has no effect unless WithExpand (or WithExpandFromOSEnv) is also set.

Any github.com/go-rotini/env Source satisfies VarLookup, so passing env.OSEnv() or env.Map(...) works directly. For the common "fall back to the process environment" case, WithExpandFromOSEnv is shorter and pulls in no dependency.

func WithMaxExpansionDepth

func WithMaxExpansionDepth(n int) ParseOption

WithMaxExpansionDepth bounds variable-expansion recursion. Default 16; a non-positive value keeps the default. The bound is reached by deeply nested or self-referential `${VAR:-default}` / `${VAR:?msg}` constructs (e.g. `${A:-${A:-${A}}}`); exceeding it yields an *ExpansionError whose cause is ErrMaxExpansionDepth. Plain references like `${A}` (and even `A=${A}`) are resolved at most once against already-resolved entries and never recurse.

func WithMaxFileSize

func WithMaxFileSize(n int64) ParseOption

WithMaxFileSize caps the total input size. Default 10 MiB. Zero or negative means unlimited.

func WithMaxLineLength

func WithMaxLineLength(n int) ParseOption

WithMaxLineLength caps a single physical line's length. Default 1 MiB. Zero or negative means unlimited. Multi-line double-quoted values count toward the line limit per physical line, not in aggregate.

func WithRelaxedNames

func WithRelaxedNames() ParseOption

WithRelaxedNames accepts identifiers containing characters outside the POSIX [A-Za-z_][A-Za-z0-9_]* class (hyphens, dots, non-ASCII).

Interaction with WithExpand: a `${...}` reference is always parsed with the Bash `${VAR-default}` / `${VAR:-default}` rules, so a hyphen inside the braces is treated as the default-value operator, not as part of the name — `${MY-KEY}` means "value of MY, or the literal `KEY` if MY is unset", even if a key literally named `MY-KEY` exists. A hyphenated key therefore cannot be interpolated via `${...}`; reference it through a parent (WithExpansionParent) keyed on the hyphenated name, or avoid hyphens in keys you interpolate. (`$NAME` without braces only ever consumes POSIX identifier bytes, so `$MY-KEY` is `<value of MY>` followed by the literal `-KEY` regardless of this option.) Dots in names do not collide with any expansion operator.

func WithStrictDotenv

func WithStrictDotenv() ParseOption

WithStrictDotenv rejects non-portable constructs: YAML-style `KEY: value` separators, the `export ` prefix, identifiers outside the POSIX class. Useful for asserting cross-dialect portability.

func WithStrictExpansion

func WithStrictExpansion() ParseOption

WithStrictExpansion makes a reference to an unset variable raise an *ExpansionError (cause ErrUnsetVariable) rather than expand to empty. Has no effect unless WithExpand (or WithExpandFromOSEnv) is also set.

type Position

type Position struct {
	File   string
	Line   int
	Column int
	Offset int
}

Position identifies a location in a `.env` source. File is the path the source was loaded from, or "" for an in-memory Parse. Line and Column are 1-based; Offset is the 0-based byte offset of the location within the source. The zero value (all fields zero) means "no position information".

Position is the location type reported by ParseError, ExpansionError, LintIssue, and Entry.

func (Position) String

func (p Position) String() string

String renders the position as "file:line:column", omitting the file when it is empty and the column when it is zero. The zero Position renders as "<unknown position>".

type Severity

type Severity int

Severity classifies a LintIssue.

const (
	// SeverityError marks a [LintIssue] that prevented parsing.
	SeverityError Severity = iota

	// SeverityWarning marks a parseable but non-portable construct
	// (YAML-style `:`, `export` prefix, backtick quoting, hyphenated
	// identifiers).
	SeverityWarning
)

func (Severity) String

func (s Severity) String() string

type Source

type Source struct {
	// contains filtered or unexported fields
}

Source is an in-memory key/value view of a parsed `.env` file, returned by NewSource / NewReaderSource / MapSource / [(*File).Source]. It is a snapshot taken at construction; later changes to the underlying file do not propagate (for live reload, layer a file watcher — e.g. from github.com/go-rotini/fs — over Parse and re-derive the source on change).

(*Source).Lookup makes *Source structurally compatible with the Source interface in github.com/go-rotini/env, so a parsed `.env` file drops straight into an env loader's source chain without dotenv depending on env.

func MapSource

func MapSource(m map[string]string) *Source

MapSource returns a *Source over a copy of m. It is handy for supplying an in-memory parent to WithExpansionParent, or in tests. The returned Source is independent of m — later changes to m are not reflected.

func NewReaderSource

func NewReaderSource(r io.Reader, opts ...ParseOption) (*Source, error)

NewReaderSource reads a `.env` byte stream from r and returns a *Source over its parsed map.

Example

A *Source's Lookup method makes a parsed file usable wherever a github.com/go-rotini/env Source is expected — no import edge needed. (set==true with an empty value means "present but empty".)

package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/go-rotini/dotenv"
)

func main() {
	src, err := dotenv.NewReaderSource(strings.NewReader("FOO=bar\nDEBUG=\n"))
	if err != nil {
		log.Fatal(err)
	}
	foo, ok := src.Lookup("FOO")
	fmt.Printf("FOO=%q (set=%t)\n", foo, ok)
	dbg, ok := src.Lookup("DEBUG")
	fmt.Printf("DEBUG=%q (set=%t)\n", dbg, ok)
	_, ok = src.Lookup("MISSING")
	fmt.Printf("MISSING set=%t\n", ok)
}
Output:
FOO="bar" (set=true)
DEBUG="" (set=true)
MISSING set=false

func NewSource

func NewSource(path string, opts ...ParseOption) (*Source, error)

NewSource reads a `.env` file from path and returns a *Source over its parsed map. The source is a snapshot taken at construction; subsequent file changes do not propagate (for live reload, layer a file watcher — e.g. from github.com/go-rotini/fs — over Parse and re-derive the source on change).

(*Source).Lookup makes the result usable wherever a github.com/go-rotini/env Source is expected, without dotenv importing env.

func (*Source) Keys

func (s *Source) Keys() []string

Keys returns the source's keys in sorted order.

func (*Source) Lookup

func (s *Source) Lookup(key string) (value string, ok bool)

Lookup returns the value associated with key and whether the key is set. An ok=true result with an empty value distinguishes "set but empty" from "unset". Safe for concurrent use; *Source is immutable after construction.

func (*Source) ToMap

func (s *Source) ToMap() map[string]string

ToMap returns a copy of the source's key/value pairs (last-write-wins per Bash convention, as resolved at parse time).

type VarLookup

type VarLookup interface {
	// Lookup returns the value associated with key and whether the key is
	// set. ok=true with an empty value means "set but empty".
	Lookup(key string) (value string, ok bool)
}

VarLookup resolves a variable name to its value. It is the contract WithExpansionParent uses to resolve `$VAR` / `${VAR}` references that are not defined in the file itself.

The single method matches the Source interface in github.com/go-rotini/env, so env.OSEnv(), env.Map(...), and any other env.Source satisfy VarLookup — dotenv does not import env to make that work. (For the common "resolve against the process environment" case, prefer WithExpandFromOSEnv, which needs no argument; for an in-memory map, MapSource returns a VarLookup.)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL