Env File Parser

Parse dotenv files with export prefixes, quoted values, inline comments, and strict key validation, returning a plain map.

GoConfig Management

Code

snippet
package main

import (
	"fmt"
	"regexp"
	"sort"
	"strconv"
	"strings"
)

var envKeyPattern = regexp.MustCompile("^[A-Za-z_][A-Za-z0-9_]*$")

func parseEnvValue(raw string) (string, error) {
	if len(raw) >= 2 && raw[0] == '\'' && raw[len(raw)-1] == '\'' {
		return raw[1 : len(raw)-1], nil
	}
	if len(raw) >= 2 && raw[0] == '"' && raw[len(raw)-1] == '"' {
		unquoted, err := strconv.Unquote(raw)
		if err != nil {
			return "", fmt.Errorf("bad quoted value: %w", err)
		}
		return unquoted, nil
	}
	if index := strings.Index(raw, " #"); index >= 0 {
		raw = strings.TrimSpace(raw[:index])
	}
	return raw, nil
}

func ParseEnv(data []byte) (map[string]string, error) {
	values := make(map[string]string)
	for number, raw := range strings.Split(string(data), "\n") {
		line := strings.TrimSpace(strings.TrimSuffix(raw, "\r"))
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		line = strings.TrimSpace(strings.TrimPrefix(line, "export "))
		key, value, ok := strings.Cut(line, "=")
		if !ok {
			return nil, fmt.Errorf("line %d: missing '='", number+1)
		}
		key = strings.TrimSpace(key)
		if !envKeyPattern.MatchString(key) {
			return nil, fmt.Errorf("line %d: invalid key %q", number+1, key)
		}
		parsed, err := parseEnvValue(strings.TrimSpace(value))
		if err != nil {
			return nil, fmt.Errorf("line %d: %w", number+1, err)
		}
		values[key] = parsed
	}
	return values, nil
}

func main() {
	document := []byte("# service settings\n" +
		"export API_URL=https://api.example.test\n" +
		"export APP_NAME=\"Demo \\\"API\\\"\"\n" +
		"EMPTY=\n" +
		"DB_PASSWORD='p@ss word'\n" +
		"PORT=8080 # public port\n")
	values, err := ParseEnv(document)
	if err != nil {
		panic(err)
	}
	keys := make([]string, 0, len(values))
	for key := range values {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	for _, key := range keys {
		fmt.Printf("%s=%q\n", key, values[key])
	}

	_, err = ParseEnv([]byte("1BAD=x"))
	fmt.Println("invalid key rejected:", err != nil)
}