envx

package module
v1.5.1 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: GPL-2.0, GPL-3.0 Imports: 11 Imported by: 0

README

envx

Go Reference Go version Test coverage OpenSSF Best Practices OpenSSF Scorecard

Typed environment-variable configuration for containerized Go apps

A tiny reader for the way containerized apps are actually configured: environment variables with sensible defaults. Every getter takes a fallback and never fails. An unset or empty variable falls back silently; a set-but-malformed value falls back with one slog Warn naming the variable, so a deployment typo shows up in the logs instead of silently changing behavior.

Two calls cover the values an app cannot default: Require returns a typed error for a missing mandatory variable, and Secret adds the Docker secrets convention (KEY_FILE pointing at a mounted file, read once, size-bounded, returned as written apart from one trailing line ending) on top.

For apps configured by a YAML file rather than the environment, the envx/yamlenv subpackage expands allowlisted ${VAR} references inside the parsed document's string values, so secrets stay in the environment while the file holds structure. Expansion opens one hole: a failing decode of the expanded document can embed a secret in its error message. SanitizeDecodeError closes it, rebuilding the error so it is safe to log at startup. yamlenv is its own nested Go module, versioned and released independently; it alone carries the YAML dependency, which the root envx module never links — plain envx requires only github.com/cplieger/pathinside (standard-library-only, and the source of the KEY_FILE path rule). Install yamlenv separately: go get github.com/cplieger/envx/yamlenv@vX.Y.Z.

Install

go get github.com/cplieger/envx@latest

Usage

addr := envx.String("APP_LISTEN", ":8080")
debug := envx.Bool("APP_DEBUG", false)          // true/1/yes/on · false/0/no/off
retries := envx.Int("APP_RETRIES", 3)
interval := envx.Duration("APP_INTERVAL", 6*time.Hour) // Go duration syntax

token, err := envx.Require("APP_TOKEN") // *envx.MissingError when unset/empty
if err != nil {
	slog.Error("startup", "error", err)
	os.Exit(1)
}

// Docker secrets: reads APP_API_KEY_FILE when set, else APP_API_KEY.
apiKey, err := envx.Secret("APP_API_KEY")

// A present-but-empty APP_API_KEY_FILE names no file, so it resolves as if
// unset. Refuse it when a broken secret mount must not fall back:
if envx.IsBlankSecretFilePath("APP_API_KEY") {
	slog.Error("startup", "error", "APP_API_KEY_FILE is set but empty")
	os.Exit(1)
}

YAML config files reference environment variables with ${VAR} and expand them after parsing, inside string values only. Load is the one-call safe pipeline: single-document check, unknown-key strictness, parse, expansion, decode, and fail-closed error sanitization, in the right order:

cfg := defaultConfig() // decode overlays the file onto your defaults
allow := func(name string) bool { return strings.HasPrefix(name, "APP_") }
unresolved, err := yamlenv.Load(data, &cfg, allow)
if len(unresolved) > 0 {
	slog.Warn("config references unset environment variables",
		"vars", strings.Join(unresolved, ","))
}
if err != nil {
	// Already safe to log: a misspelled key, a second document, and any
	// parse/decode failure come back sanitized (no expanded secret can
	// survive into the message).
	return err
}

The pieces Load composes (Expand, SanitizeDecodeError, CheckUnknownKeys, CheckSingleDocument) stay exported for pipelines with different policy and are documented individually below.

API

  • String(key, fallback string) string: value or fallback; empty counts as unset.
  • Bool(key string, fallback bool) bool: tolerant parse (true/1/yes/on, false/0/no/off, case-insensitive, trimmed); malformed → Warn + fallback.
  • Int(key string, fallback int) int: strconv.Atoi on the trimmed value; malformed → Warn + fallback.
  • Duration(key string, fallback time.Duration) time.Duration: time.ParseDuration syntax (30s, 6h, 1h30m); a bare unitless number is rejected (ambiguous) → Warn + fallback.
  • BoolStrict(key string) (bool, bool, error): Bool's parser with the result owned by the caller instead of Warn + fallback: unset/empty → (false, false, nil), malformed → (false, false, err), valid → (b, true, nil). ok, not the value, distinguishes "set to false" from "not set". Never logs, and the error names the key and the accepted spellings but never the value. Prefer it over Bool for a key whose value may be sensitive — one an operator could wire to a secret by mistake, since Bool's Warn line carries the raw value — or when the caller owns its own diagnostics.
  • IntStrict(key string) (int, bool, error) / DurationStrict(key string) (time.Duration, bool, error): the parse result owned by the caller instead of Warn + fallback: unset/empty → (0, false, nil), malformed → (0, false, err) naming the key, valid → (v, true, nil). Never logs.
  • Require(key string) (string, error): value, or *MissingError (carries Key) when unset or empty. Returns an error rather than exiting so a caller can collect every missing variable and fail once.
  • Secret(key string) (string, error): KEY_FILE (mounted secret file: single-handle bounded read, 1 MB cap, traversal-rejected, with at most one trailing line ending removed) wins over KEY. The secret value never appears in an error or log line.
  • SecretWithSource(key string) (string, SecretSource, error): Secret plus the channel that supplied the value (SourceFile, SourceEnv, SourceNone), reported on the error paths too, so a caller can warn that a KEY it also set was ignored in favour of KEY_FILE.
  • IsBlankSecretFilePath(key string) bool: reports a KEY_FILE that is present but blank (empty or whitespace only) and therefore names no file. Resolution is unchanged — it still falls through as if unset — but the caller can refuse it, which matters when the secret is optional and the fallthrough is fail-open.
  • ErrBlankSecretFile: the sentinel for a KEY_FILE naming a readable file whose content is blank (empty or whitespace only), so a caller's allow-empty policy can cover both delivery channels identically.
  • ErrSecretFilePathRejected, ErrSecretFileTooLarge, ErrSecretFileGrew, ErrSecretFileUnreadable: the remaining secret-file failure classes, each detectable with errors.Is, so a caller can report WHY the file was unusable without matching error text and without echoing the operator-supplied path (which is the leak risk when KEY_FILE was misconfigured to hold the secret itself). The OS-failure class keeps its *os.PathError reachable with errors.As.
  • MissingError{Key}: the typed missing-variable error, detectable with errors.As.
  • yamlenv.Load(data []byte, out any, allow func(name string) bool, opts ...LoadOption) (unresolved []string, err error): the composed safe loading pipeline in one call: single-document check, unknown-key strictness, parse, Expand with the caller's allowlist, decode into out (a non-nil pointer pre-populated with defaults), and sanitized errors on every failure path. Options: WithSanitizeOptions(...) forwards sanitizer policy; WithErrorPassthrough(pred) returns caller-owned decode errors unchanged.
  • yamlenv.Expand(root *yaml.Node, allow func(name string) bool) (unresolved []string): in-place expansion of allowlisted, set ${VAR} references inside a parsed document's string scalar values; post-parse, so an environment value can never change the document structure; everything else stays byte-for-byte literal. Returns the allowlisted names left unresolved, for the caller to warn on.
  • yamlenv.SanitizeDecodeError(err error, opts ...SanitizeOption) error: rebuild a yaml.v3 parse or decode error from its value-independent structure (line numbers, source tags, destination types) so no fragment of a document value, possibly an expanded secret, survives into the message; WithUnknownKeyEcho() opts into keeping the unknown-key name. The returned error never wraps the original.
  • yamlenv.CheckUnknownKeys(data []byte, probe any) error: fail loudly on a key the config type does not declare, via a KnownFields(true) re-decode of the raw pre-expansion document into probe. The returned error may embed document content; log it through SanitizeDecodeError.
  • yamlenv.CheckSingleDocument(data []byte) error: reject input carrying more than one YAML document, so nothing below a stray --- separator is silently dropped. The only non-nil return is the static ErrMultipleDocuments, safe to log unsanitized.

Full contracts (trim rules, the expansion grammar, the sanitizer's entry shapes, the probe's value-error filtering) are in the package documentation: envx and yamlenv.

Behavior contract

  • Empty equals unset. Compose files and CI matrices routinely materialize KEY= for a knob the operator left blank; every getter treats that as absence. Use os.LookupEnv directly in the rare case the distinction matters. The one distinction the package answers itself is a blank KEY_FILE (IsBlankSecretFilePath): that variable holds a pointer, not a value, so its blankness is a statement about this package's own channel selection rather than about the app's value semantics.
  • Malformed values are visible, never fatal. The one Warn line (through slog.Default()) carries key, the raw value, the expected kind, and the fallback used. Config values are not secrets; Secret never routes through this path. The strict variants (BoolStrict, IntStrict, DurationStrict) return the malformed value as an error instead and never log; the caller owns the decision, which is also the way to read a key whose value could turn out to be sensitive.
  • A tolerant getter and its strict variant share one parser. Bool/BoolStrict, Int/IntStrict and Duration/DurationStrict accept exactly the same values by construction, not by convention, so the two layers cannot drift apart; only the malformed-value policy differs (Warn + fallback, or an error).
  • Parsing getters trim; String does not. Bool, Int, Duration, and the strict variants parse the whitespace-trimmed value; String returns the raw value because whitespace can be meaningful in a free-form string (a whitespace-only value counts as set).
  • A secret is never rewritten. Both Secret channels return the credential as configured: KEY verbatim, and KEY_FILE's content with at most ONE trailing line ending (\n or \r\n) removed, because an editor or kubectl create secret --from-file appends one and a file cannot store a value without that ambiguity. Edge spaces, tabs, non-breaking spaces and a second trailing newline are content and survive, so a caller that validates a credential verbatim gets the same verdict from either channel. Blankness is the one judgement made on the trimmed content: a file holding only whitespace is ErrBlankSecretFile, a broken mount rather than a secret.
  • No state, no goroutines, no import-time reads. The process environment is read at call time only.

Unsupported by Design

Deliberate non-goals, not TODOs:

Feature Rationale
Struct tags / reflection-based config loading This is a getter library, not a config framework. An app's config struct assembles itself from explicit calls, which keeps every default and key name greppable.
.env file loading The container runtime (compose, Kubernetes) owns materializing the environment; a second loader creates precedence questions with no consumer need.
Float / slice / map getters No consumer parses these from the environment. Added only when a real app needs one.
Prefix namespacing (WithPrefix("APP_")) Key names stay greppable verbatim; a prefix helper saves a few characters and costs discoverability.
Panic-on-missing (MustX) Require returns an error so startup can report every missing variable at once instead of dying on the first.

Contributing

Issues and PRs are welcome. See CONTRIBUTING.md for the conventions and how to run the checks locally.

Disclaimer

This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.

This project was built with AI-assisted tooling using Claude, GPT, and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.

License

GPL-3.0. See LICENSE.

Documentation

Overview

Package envx reads typed configuration from environment variables, the standard way a containerized app is configured.

Every getter takes a fallback and never fails: an unset or empty variable returns the fallback silently, and a set-but-malformed value returns the fallback with one Warn line through slog's default logger, so a typo in a deployment surfaces in the logs instead of silently changing behavior. Boolean parsing is tolerant (true/1/yes/on, false/0/no/off, case-insensitive, trimmed) because that is what deployment YAML tends to contain.

Two calls handle the values an app cannot default: Require returns an error for a missing mandatory variable, and Secret additionally supports the Docker convention of an adjacent KEY_FILE variable pointing at a mounted secret file, read once, size-bounded, and returned as written apart from one trailing line ending. Each way a secret file can be unusable is named by a sentinel, so a caller can report the class with errors.Is instead of matching message text or echoing the configured path. A KEY_FILE that is present but blank names no file, so it resolves as if unset; IsBlankSecretFilePath reports that state for the caller who must refuse a broken secret pointer rather than fall back to the plain variable.

For the caller that must own the malformed-value decision instead of accepting warn-and-fallback — reject startup, apply bounds, keep an existing value — BoolStrict, IntStrict and DurationStrict return the parse result as (value, ok, error) and never log. BoolStrict shares Bool's parser, so the two accept exactly the same spellings; prefer it over Bool for a key whose value may be sensitive (the Warn line names the raw value) or when the caller owns its own diagnostics.

envx reads the process environment at call time; it holds no state, starts no goroutines, and depends on nothing beyond the standard library and github.com/cplieger/pathinside, the standard-library-only path-name predicates the Secret file rule is built from.

Example
package main

import (
	"fmt"
	"os"
	"time"

	"github.com/cplieger/envx"
)

func main() {
	os.Setenv("APP_LISTEN", ":9090")
	os.Setenv("APP_DEBUG", "yes")
	defer os.Unsetenv("APP_LISTEN")
	defer os.Unsetenv("APP_DEBUG")

	addr := envx.String("APP_LISTEN", ":8080")
	debug := envx.Bool("APP_DEBUG", false)
	interval := envx.Duration("APP_INTERVAL", 6*time.Hour)

	fmt.Println(addr, debug, interval)
}
Output:
:9090 true 6h0m0s

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrSecretFilePathRejected means KEY_FILE named a path this package
	// refuses to open: not already clean, or containing a ".." path component.
	ErrSecretFilePathRejected = errors.New("envx: secret file path rejected")
	// ErrSecretFileTooLarge means the file was already over maxSecretFileSize
	// when it was opened.
	ErrSecretFileTooLarge = errors.New("envx: secret file exceeds the size limit")
	// ErrSecretFileGrew means the file passed the size gate and then grew past
	// maxSecretFileSize while it was being read, so the content would have been
	// silently truncated.
	ErrSecretFileGrew = errors.New("envx: secret file grew past the size limit during read")
	// ErrSecretFileUnreadable means the operating system refused the open, stat
	// or read. The underlying *os.PathError stays reachable with errors.As, so
	// a caller can still report the syscall and its reason (without the path,
	// which the PathError also carries).
	ErrSecretFileUnreadable = errors.New("envx: secret file could not be read")
)

The failure classes of a KEY_FILE secret read. Every error Secret and SecretWithSource return for a configured secret file wraps exactly one of these or ErrBlankSecretFile, so a caller can name WHY the file was unusable with errors.Is and apply a different policy per class.

The classes exist because the alternative was worse: without them the only way to tell a rejected path from an oversized file was matching this package's error TEXT, so callers folded every non-blank failure into one generic message instead. That mattered here more than in most packages, because these errors embed the operator-supplied PATH and a caller whose KEY_FILE was misconfigured to hold the secret ITSELF (rather than a path to it) must be able to describe the failure without echoing the value into a log. Naming the class no longer requires touching the path.

The classes are deliberately narrow rather than one "unusable file" sentinel: their remediations differ (fix the variable, shrink the file, stop rewriting it, fix the mount) and only a caller writing an operator-facing message knows which of those it wants to say.

View Source
var ErrBlankSecretFile = errors.New("envx: secret file is blank")

ErrBlankSecretFile is returned by Secret and SecretWithSource when KEY_FILE names a readable file whose content is blank — empty, or whitespace only.

Blankness is the one judgement made on the whitespace-trimmed content: a file holding only spaces or newlines is a broken secret mount, not a secret. The value a readable file DOES carry is returned with at most a trailing line ending removed, so the two operations do not share a rule (see trimTrailingLineEnding).

It is a distinct sentinel because a caller often has a policy for a blank secret that differs from its policy for an unusable one. cert-converter is the motivating case: it offers PFX_ALLOW_EMPTY_PASSWORD as an explicit opt-in to running without a password, and that opt-in has to mean the same thing whether the blank value arrived through the environment or through a mounted file. Without a sentinel the file channel could only be classified by matching this package's error TEXT, so the opt-in silently applied to one channel and not the other.

It is deliberately NOT a *MissingError: the operator did configure a secret file, and treating "you forgot to set this" the same as "the file you mounted is blank" would hide a broken secret mount behind a caller's default.

Functions

func Bool

func Bool(key string, fallback bool) bool

Bool returns the boolean value of the environment variable key, or fallback when the variable is unset or empty.

Parsing is tolerant of the spellings deployment files actually contain: true/1/yes/on and false/0/no/off, case-insensitive, surrounding whitespace ignored. Any other value logs one Warn through slog's default logger and returns fallback, so a typo ("ture") is visible in the logs instead of silently flipping a flag.

The Warn line carries the raw value. Use BoolStrict for a key whose value may be sensitive, or when the caller owns its own diagnostics.

func BoolStrict added in v1.5.0

func BoolStrict(key string) (value, ok bool, err error)

BoolStrict returns the boolean value of the environment variable key with the parse result owned by the caller: a set-but-malformed value is returned as an error instead of the tolerant getters' warn-and-fallback.

The accepted spellings are exactly Bool's (true/1/yes/on and false/0/no/off, case-insensitive, surrounding whitespace ignored) because both route through the same parser and cannot drift apart. The three states match IntStrict: unset or empty (false, false, nil), malformed (false, false, err), valid (b, true, nil). value is false in two of those three, so ok — never value alone — distinguishes "set to false" from "not set".

Unlike IntStrict and DurationStrict, the error carries no fragment of the value: there is no parse error to wrap, and the reason to reach for BoolStrict is usually that the value must not be repeated anywhere. It names the key and the accepted vocabulary. Strict variants never log.

Use Bool when a malformed value should fall back with a Warn naming the raw value; use BoolStrict for a key whose value may be sensitive (one an operator could wire to a secret by mistake, so no diagnostic may echo it) or when the caller owns its own diagnostics.

(value and ok share their bool type in the signature at the linter's insistence; they mean what the family's other strict variants mean.)

func Duration

func Duration(key string, fallback time.Duration) time.Duration

Duration returns the value of the environment variable key parsed with time.ParseDuration ("30s", "6h", "1h30m"), or fallback when the variable is unset or empty. A set-but-unparseable value logs one Warn through slog's default logger and returns fallback.

A bare number without a unit is deliberately not accepted: "30" is ambiguous between seconds and minutes across tools, and time.ParseDuration rejecting it (with the Warn line naming the key) is clearer than guessing.

func DurationStrict added in v1.2.0

func DurationStrict(key string) (value time.Duration, ok bool, err error)

DurationStrict returns the value of the environment variable key parsed with time.ParseDuration ("30s", "6h", "1h30m"), with the parse result owned by the caller: a set-but-malformed value is returned as an error instead of the tolerant getters' warn-and-fallback.

The three states match IntStrict: unset or empty (0, false, nil), malformed (0, false, err), valid (d, true, nil). As with Duration, a bare number without a unit is rejected ("30" is ambiguous between seconds and minutes across tools). Strict variants never log.

func Int

func Int(key string, fallback int) int

Int returns the integer value of the environment variable key, or fallback when the variable is unset or empty. A set-but-unparseable value logs one Warn through slog's default logger and returns fallback.

func IntStrict added in v1.2.0

func IntStrict(key string) (value int, ok bool, err error)

IntStrict returns the integer value of the environment variable key with the parse result owned by the caller: a set-but-malformed value is returned as an error instead of the tolerant getters' warn-and-fallback.

ok reports a successfully parsed value; it is false when the variable is unset or empty (empty equals unset, as with every getter) and false when the value did not parse. err is non-nil only for a set-but-malformed value; it names the key and wraps the underlying strconv error. Exactly one of the three states holds: unset (0, false, nil), malformed (0, false, err), or valid (n, true, nil). Strict variants never log.

Use Int when a malformed value should fall back with a Warn; use IntStrict when the caller must decide what a malformed value means (reject startup, apply bounds, keep an existing value).

func IsBlankSecretFilePath added in v1.4.0

func IsBlankSecretFilePath(key string) bool

IsBlankSecretFilePath reports whether the KEY_FILE companion variable for key is present in the environment but blank — set to the empty string, or to whitespace only — and therefore names no secret file.

SecretWithSource cannot report this state, and deliberately still does not: it gates the file channel on a non-empty KEY_FILE, so `KEY_FILE=` is indistinguishable from unset and resolves through KEY exactly as if the operator had never written it. That is the right rule for a VALUE, which is why every getter here treats empty as unset: compose files and CI matrices materialize `KEY=` for a knob left blank. KEY_FILE holds a POINTER, though, and a blank pointer is a broken secret mount rather than an operator declining to configure one — `KEY_FILE=${SECRETS_DIR}/token` with SECRETS_DIR undefined produces exactly this shape.

The state is reported instead of acted on because only the caller knows what its secret's absence means, and the two answers are far apart. For a mandatory credential the fallthrough surfaces as *MissingError and startup fails anyway; for an OPTIONAL one — a bearer token whose absence serves an endpoint unauthenticated — a blank KEY_FILE silently disarms the gate, so the fallthrough is fail-OPEN. A caller that must refuse that asks before resolving:

if envx.IsBlankSecretFilePath("BEAT_TOKEN") {
	return errors.New("BEAT_TOKEN_FILE is set but empty: unset it to configure BEAT_TOKEN directly, or point it at a secret file")
}
token, src, err := envx.SecretWithSource("BEAT_TOKEN")

Whitespace counts as blank, the rule ErrBlankSecretFile already applies to a secret file's CONTENT: a whitespace-only path is a quoting slip, never a filename an operator meant to write. Resolution treats the two shapes differently — an empty KEY_FILE is ignored, a whitespace-only one is opened and fails — so for that shape this predicate buys a diagnostic naming the blank variable in place of a "no such file or directory" for a filename nobody can see, not a change of fail direction.

It reports the state and changes none of it: a deployment where `KEY_FILE=` falls through to KEY keeps doing so, including for callers that never ask.

Example
package main

import (
	"fmt"
	"os"

	"github.com/cplieger/envx"
)

func main() {
	// The shape compose interpolation of an undefined variable produces: the
	// pointer is present, but it names no file.
	os.Setenv("APP_API_KEY_FILE", "")
	os.Setenv("APP_API_KEY", "inline-key")
	defer os.Unsetenv("APP_API_KEY_FILE")
	defer os.Unsetenv("APP_API_KEY")

	// Resolution cannot see it: the inline value wins as if the operator had
	// never written APP_API_KEY_FILE.
	_, source, _ := envx.SecretWithSource("APP_API_KEY")
	fmt.Println("resolved from:", source)
	fmt.Println("blank secret pointer:", envx.IsBlankSecretFilePath("APP_API_KEY"))
}
Output:
resolved from: env
blank secret pointer: true

func Require

func Require(key string) (string, error)

Require returns the value of the environment variable key, or a *MissingError when it is unset or empty. It returns an error rather than exiting so the caller controls startup failure (collect every missing key, log through the configured handler, then exit once).

Example
package main

import (
	"fmt"

	"github.com/cplieger/envx"
)

func main() {
	// Collect every missing variable, then fail once.
	var missing []error
	if _, err := envx.Require("APP_TOKEN_UNSET_A"); err != nil {
		missing = append(missing, err)
	}
	if _, err := envx.Require("APP_TOKEN_UNSET_B"); err != nil {
		missing = append(missing, err)
	}
	fmt.Println(len(missing), "missing")
}
Output:
2 missing

func Secret

func Secret(key string) (string, error)

Secret returns a required secret from the environment, supporting the Docker secrets convention: when KEY_FILE is set to a non-empty value, the secret is read from that file (size-bounded, with at most one trailing line ending removed); otherwise the value of KEY itself is returned. An unset or empty result is a *MissingError, and a configured file whose content is blank is ErrBlankSecretFile.

The file channel returns the secret as written apart from that single trailing newline, so a value whose leading or trailing SPACES are part of the credential survives the round trip: the two channels must not disagree about what the secret IS, and KEY is never rewritten either. Only the line ending an editor or `kubectl create secret` appends is removed, because that one is an artifact of storing a value in a file rather than part of the value.

The KEY_FILE indirection keeps the secret value out of `docker inspect` output and compose files; the file path must be clean (no ".." traversal), and the read uses a single handle so the size check and the read cannot race. The secret value itself never appears in an error or a log line; errors carry the key name and file path only. Each failure class is named by a sentinel (ErrSecretFilePathRejected, ErrSecretFileTooLarge, ErrSecretFileGrew, ErrSecretFileUnreadable, ErrBlankSecretFile) so a caller can report WHY the file was unusable without echoing the path.

Use SecretWithSource when the caller needs to know WHICH channel supplied the value — for example to warn that a KEY it also set was ignored in favour of KEY_FILE. Secret delegates to it, so the two cannot drift. Use IsBlankSecretFilePath when a KEY_FILE that is present but blank must be refused instead of read as absent.

func String

func String(key, fallback string) string

String returns the value of the environment variable key, or fallback when the variable is unset or empty. An empty value is treated as unset because compose files and CI matrices routinely materialize `KEY=` for a knob the operator left blank; distinguishing that from absence is almost never what a config reader wants (use os.LookupEnv directly when it is).

Unlike the parsing getters (Bool, Int, Duration), String does not trim the value: whitespace can be meaningful in a free-form string, and the caller knows whether its value is a path, a token, or a list. A whitespace-only value therefore counts as set.

Types

type MissingError

type MissingError struct {
	// Key is the environment variable name that was required.
	Key string
}

MissingError reports a required environment variable that is unset or empty. It carries the key so a caller can aggregate several missing variables into one startup failure.

func (*MissingError) Error

func (e *MissingError) Error() string

Error implements the error interface.

type SecretSource added in v1.3.0

type SecretSource string

SecretSource reports which channel a secret value came from.

const (
	// SourceNone means no value was found in either channel.
	SourceNone SecretSource = "none"
	// SourceEnv means the value came from KEY itself.
	SourceEnv SecretSource = "env"
	// SourceFile means the value came from the file named by KEY_FILE.
	SourceFile SecretSource = "file"
)

The secret delivery channels SecretWithSource distinguishes.

func SecretWithSource added in v1.3.0

func SecretWithSource(key string) (value string, source SecretSource, err error)

SecretWithSource is Secret, additionally reporting which channel supplied the value.

Secret resolves KEY_FILE ahead of KEY and returns only the value, so a caller cannot tell whether an environment variable it also set was used or silently ignored. That matters when both are set: the operator expressed two intentions and exactly one takes effect. Only the caller knows whether that is worth a warning in its own configuration vocabulary, so this reports the fact rather than logging it here — consistent with this package's rule that the caller owns the policy and envx owns the parse.

A caller that needs the both-channels-set condition composes it in one line:

v, src, err := envx.SecretWithSource("PFX_PASSWORD")
if src == envx.SourceFile && os.Getenv("PFX_PASSWORD") != "" { /* warn */ }

That works on the error paths too, which is where it matters most: an unreadable file still reports SourceFile, so the caller can tell the operator that the environment variable they also set is not a fallback.

Precedence is unchanged from Secret: KEY_FILE wins when set to a non-empty value, because the entire point of the file channel is keeping the value out of the process environment. A KEY_FILE that is present but blank names no file and is therefore not the file channel at all: resolution falls through to KEY, and IsBlankSecretFilePath is how a caller detects that a secret pointer it was given is broken.

Directories

Path Synopsis
yamlenv module

Jump to

Keyboard shortcuts

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