dotenv

package module
v0.0.0-...-a92f1ce Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 3 Imported by: 0

README

go-ruby-dotenv/dotenv

dotenv — go-ruby-dotenv

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Ruby's dotenv gem's .env parser — the deterministic, interpreter-independent core of Dotenv::Parser.call (dotenv 3.2.0). It parses the .env file format into an insertion-ordered set of key/value pairs, byte-faithful to MRI, with full variable interpolation and command-substitution parsing — without any Ruby runtime and without touching the process environment.

It is the dotenv backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-yaml and go-ruby-erb.

What it is — and isn't. Parsing the .env grammar — the LINE regex, quote stripping, escape handling, $VAR / ${VAR} interpolation, and the $(command) grammar — is fully deterministic and needs no interpreter, so it lives here as pure Go. The two genuinely host-side pieces are surfaced as explicit seams, not performed here:

  • Mutating ENV — what Dotenv.load does after parsing — is the host's job via Env.Set.
  • Executing $(command) — the gem shells out — is the host's job via Env.RunCommand. With no runner, a command parses to the empty string and is recorded (key + variable-expanded script) for the host to run; with a runner, it executes inline so command → variable → command chains resolve exactly as MRI's shelling-out does.

Features

Faithful port of Dotenv::Parser, validated against the dotenv gem on every supported platform:

  • Every value form — unquoted (trailing whitespace and inline # comments trimmed), single-quoted (literal, no interpolation), and double-quoted (with the \n \t \r \" escapes and $VAR / ${VAR} interpolation).
  • Line grammarKEY=value, export KEY=value, YAML-style KEY: value, KEY= (empty), bare KEY, blank lines, full-line and inline # comments, keys with . in them, and a leading UTF-8 BOM.
  • Variable interpolation$VAR / ${VAR} referencing earlier keys then the ambient environment, \$ to escape it, and the exact brace / empty-name / case-insensitive edge cases of the gem's VARIABLE regex.
  • Command substitution — the $(command) grammar with fully balanced nested parentheses, inner variable expansion, and \$(...) escaping; execution is a host seam (see above).
  • Multiline quoted values spanning newlines, \r\n / \r normalisation, and the default / legacy DOTENV_LINEBREAK_MODE \n / \r expansion.
  • overwrite semantics — a key already set in the environment keeps its value unless overwriting (with DOTENV_LINEBREAK_MODE exempt), and the export KEY unset-variable FormatError.

CGO-free, dependency-free, 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x).

Install

go get github.com/go-ruby-dotenv/dotenv

Usage

package main

import (
	"fmt"
	"os"

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

func main() {
	src := `HOST=example.com
export URL="https://$HOST/api"
# a comment
DEBUG=true`

	// Pure parse (Dotenv::Parser.call). No env is read or written.
	h, err := dotenv.Parse(src, false, os.LookupEnv)
	if err != nil {
		panic(err)
	}
	u, _ := h.Get("URL")
	fmt.Println(u) // https://example.com/api

	// Load semantics (Dotenv.load): parse + set into the environment via seams.
	env := &dotenv.Env{Lookup: os.LookupEnv, Set: func(k, v string) { os.Setenv(k, v) }}
	dotenv.Load(src, false, env)
}

Command substitution seam

The gem runs $(command) by shelling out. Here that is a host seam:

// Record only (pure): value becomes "", commands are returned for the host.
h, cmds, _ := dotenv.ParseWithCommands("SHA=$(git rev-parse HEAD)", false, nil)
// cmds[0] == dotenv.Command{Key: "SHA", Script: "git rev-parse HEAD"}

// Execute inline via a runner (chains resolve as in MRI):
run := func(script string) string {
	out, _ := exec.Command("/bin/sh", "-c", script).Output()
	return string(out) // chomped by the parser
}
h, _, _ = dotenv.ParseWithRunner("FOO=$(echo bar)\nBAR=$(echo $FOO)", false, nil, run)
// h["FOO"] == "bar", h["BAR"] == "bar"

API

// Parse is Dotenv::Parser.call: parse src into an insertion-ordered map.
// overwrite maps to the gem's overwrite: keyword; lookupEnv stands in for ENV
// (nil = empty). $(command) substitutions yield "".
func Parse(src string, overwrite bool, lookupEnv func(string) (string, bool)) (*OrderedMap, error)

// ParseWithCommands also returns the parsed $(command) substitutions (Script is
// the variable-expanded command text the gem would run).
func ParseWithCommands(src string, overwrite bool, lookupEnv func(string) (string, bool)) (*OrderedMap, []Command, error)

// ParseWithRunner wires the command-execution seam: runCommand runs inline and
// its chomped output is spliced in, so chains resolve.
func ParseWithRunner(src string, overwrite bool, lookupEnv func(string) (string, bool), runCommand func(script string) string) (*OrderedMap, []Command, error)

// ParseString / Load use the Env seam (Lookup / Set / RunCommand). Load mutates
// via Env.Set (Dotenv.load) and returns the parsed pairs + commands.
func ParseString(src string, overwrite bool, env *Env) (*OrderedMap, error)
func Load(src string, overwrite bool, env *Env) (*OrderedMap, []Command, error)

type Env struct {
	Lookup     func(string) (string, bool) // stands in for ENV reads
	Set        func(key, val string)       // Dotenv.load's ENV write (host seam)
	RunCommand func(script string) string  // $(command) execution (host seam)
}

type Command struct { Key, Script string }     // a parsed $(command)
type FormatError struct { Line string }        // export KEY with no value

type OrderedMap struct { /* insertion-ordered string->string */ }
func NewOrderedMap() *OrderedMap
func (m *OrderedMap) Set(key, val string)
func (m *OrderedMap) Get(key string) (string, bool)
func (m *OrderedMap) Has(key string) bool
func (m *OrderedMap) Len() int
func (m *OrderedMap) Keys() []string
func (m *OrderedMap) Map() map[string]string
func (m *OrderedMap) Each(fn func(key, val string))

Tests & coverage

The suite pairs deterministic, ruby-free golden tests (a corpus captured from the real dotenv gem, testdata/golden.json, embedded via go:embed) with a live differential MRI oracle (oracle_test.go) that re-derives the same corpus against the system ruby + dotenv gem — gated on RUBY_VERSION >= "4.0" — and also confirms the overwrite path and that the recorded command scripts, when run, reproduce MRI's inlined results. The golden tests alone hold coverage at 100%, so the qemu cross-arch and Windows lanes (where the oracle skips itself) still pass the gate.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-dotenv/dotenv authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package dotenv is a pure-Go (CGO=0) reimplementation of Ruby's `dotenv` gem (bkeepers/dotenv 3.2.0) `.env` file parser — the deterministic, interpreter-independent core of `Dotenv::Parser.call`. It parses the `.env` format into an insertion-ordered set of key/value pairs, byte-faithful to MRI, with variable interpolation and command-substitution parsing, but WITHOUT any Ruby runtime and WITHOUT touching the process environment.

The pieces that are genuinely a host concern — mutating `ENV` (what `Dotenv.load` does after parsing) and executing `$(command)` substitutions (which the gem does by shelling out) — are surfaced as explicit seams rather than performed here, keeping the package pure compute.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Load

func Load(src string, overwrite bool, env *Env) (*OrderedMap, []Command, error)

Load parses src and then sets each parsed pair into the environment via env.Set, mirroring `Dotenv.load` (parse + update ENV). It honours the gem's rule that a key already present in the environment is not overwritten unless overwrite is true (the parse already resolves such keys to their existing value). The mutation is entirely the host's Env.Set seam; the parsed OrderedMap is returned so the host can inspect exactly what was applied. Parsed `$(command)` substitutions are available via the returned Commands, since executing a shell is a host concern.

func ParseWithCommands

func ParseWithCommands(src string, overwrite bool, lookupEnv func(string) (string, bool)) (*OrderedMap, []Command, error)

ParseWithCommands is Parse but also returns the parsed `$(command)` substitutions (the host-execution seam) in source order. With no runner every command substitutes "" (the pure result); to have chains resolve, supply a command runner via ParseWithRunner.

func ParseWithRunner

func ParseWithRunner(src string, overwrite bool, lookupEnv func(string) (string, bool), runCommand func(script string) string) (*OrderedMap, []Command, error)

ParseWithRunner is Parse with the command-execution seam wired: runCommand is invoked inline during parsing for each `$(command)` (with the variable-expanded script), and its chomped return value is spliced into the value — so a command feeding a later variable or command resolves exactly as MRI's shelling-out does. A nil runCommand behaves like Parse (commands become ""). It also returns the recorded commands.

Types

type Command

type Command struct {
	// Key is the .env key whose value contained the substitution.
	Key string
	// Script is the command text after inner `$VAR`/`${VAR}` expansion, exactly
	// what the gem passes to the shell (its result is chomped and inlined).
	Script string
}

Command is a parsed `$(...)` shell-command substitution the gem would execute. The parser records each one (with the fully variable-expanded Script the gem would pass to the shell) and substitutes the empty string in its place, leaving actual execution to the host.

type Env

type Env struct {
	Lookup func(string) (string, bool)
	Set    func(key, val string)
	// RunCommand is the command-execution seam (the gem shells out for `$(cmd)`).
	// When set, ParseString/Load run it inline for each command and splice the
	// chomped output, so chains resolve as in MRI. When nil, commands yield "".
	RunCommand func(script string) string
}

Env abstracts the ambient environment the gem reaches through `ENV`: a lookup (for existing-variable checks and `$VAR` interpolation) and a setter (what `Dotenv.load` performs after parsing). The whole struct is the HOST SEAM — a host wiring this to `os.LookupEnv` / `os.Setenv`, to a `go-embedded-ruby` ENV object, or to an in-memory map decides where mutation lands. Both fields are optional; a nil Lookup behaves like an empty environment and a nil Set makes Load a no-op mutation (it still returns the parsed pairs).

type FormatError

type FormatError struct{ Line string }

FormatError is raised when a line declares an exported variable with no value and no prior assignment, mirroring Ruby's `Dotenv::FormatError`.

func (*FormatError) Error

func (e *FormatError) Error() string

type OrderedMap

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

OrderedMap is an insertion-ordered string->string map — the Go analogue of the Ruby `Hash` that `Dotenv::Parser.call` returns. Ruby hashes preserve insertion order, and dotenv relies on it (later keys may interpolate earlier ones), so the order is part of the observable, byte-faithful result.

func NewOrderedMap

func NewOrderedMap() *OrderedMap

NewOrderedMap returns an empty OrderedMap.

func Parse

func Parse(src string, overwrite bool, lookupEnv func(string) (string, bool)) (*OrderedMap, error)

Parse parses the `.env`-format src into an OrderedMap, byte-faithful to `Dotenv::Parser.call`.

  • overwrite mirrors the gem's `overwrite:` keyword. When false, a key already present in the ambient environment (per lookupEnv) keeps that ambient value — except "DOTENV_LINEBREAK_MODE", which is always taken from the file.
  • lookupEnv stands in for `ENV`: it resolves existing-variable checks and `$VAR` interpolation fallbacks. Pass nil for an empty environment.

It returns a FormatError if an `export KEY` line names a variable that was never assigned. Any `$(command)` substitutions are parsed, expanded, replaced by "" in the value, and returned via Commands for the host to execute.

func ParseString

func ParseString(src string, overwrite bool, env *Env) (*OrderedMap, error)

ParseString parses src with the given Env, mirroring `Dotenv.parse` on a single source string (the gem's `Dotenv::Parser.call`). It does not mutate the environment. overwrite maps to the gem's `overwrite:` keyword. If the Env supplies a RunCommand, `$(cmd)` substitutions execute inline via that seam.

func (*OrderedMap) Each

func (m *OrderedMap) Each(fn func(key, val string))

Each calls fn for every pair in insertion order.

func (*OrderedMap) Get

func (m *OrderedMap) Get(key string) (string, bool)

Get returns the value for key and whether it is present.

func (*OrderedMap) Has

func (m *OrderedMap) Has(key string) bool

Has reports whether key is present (Ruby Hash#member?).

func (*OrderedMap) Keys

func (m *OrderedMap) Keys() []string

Keys returns the keys in insertion order (a copy; safe to mutate).

func (*OrderedMap) Len

func (m *OrderedMap) Len() int

Len returns the number of pairs.

func (*OrderedMap) Map

func (m *OrderedMap) Map() map[string]string

Map returns a plain (unordered) map copy of the pairs.

func (*OrderedMap) Set

func (m *OrderedMap) Set(key, val string)

Set assigns key=val, appending the key on first insertion and preserving its original position on update (Ruby Hash#[]= semantics).

type Parser

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

Parser holds the state of a single `.env` parse: the (line-normalised) source, the accumulated ordered hash, the overwrite flag, and the environment lookup seam standing in for MRI's `ENV`.

Jump to

Keyboard shortcuts

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