// Package config loads ENKLY_* configuration from a ".env" file into the // process environment, so `os.Getenv` calls throughout the app (see // getenv() in cmd/server/main.go) pick up values from ".env" the same // way they'd pick up real environment variables — without adding a // third-party dependency like godotenv (this module intentionally keeps // go.mod to lib/pq + gomarkdown; see the comment on that file). package envkit import ( "bufio" "fmt" "os" "strings" ) // LoadDotEnv reads simple KEY=VALUE lines from path and calls // os.Setenv for each one whose key isn't already set in the real // environment — actual environment variables always win, so // `ENKLY_SMTP_PASSWORD=x make run` still overrides whatever ".env" // says. Missing files are not an error: ".env" is optional (e.g. in // docker-compose.yml, config comes from `environment:` instead). // // Supported syntax, deliberately small: // // KEY=value // KEY="value with spaces, or a # that isn't a comment" // KEY='value' // # full-line comments and blank lines are ignored // export KEY=value (a leading "export " is stripped, for .env files // written to be `source`-able in a shell too) // // No variable interpolation, no multi-line values. func LoadDotEnv(path string) error { f, err := os.Open(path) if err != nil { if os.IsNotExist(err) { return nil } return fmt.Errorf("config: open %s: %w", path, err) } defer f.Close() scanner := bufio.NewScanner(f) lineNo := 0 for scanner.Scan() { lineNo++ line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } line = strings.TrimPrefix(line, "export ") key, value, ok := strings.Cut(line, "=") if !ok { return fmt.Errorf("config: %s:%d: expected KEY=VALUE, got %q", path, lineNo, line) } key = strings.TrimSpace(key) value = strings.TrimSpace(value) if key == "" { return fmt.Errorf("config: %s:%d: empty key", path, lineNo) } // Strip a single matching pair of surrounding quotes, if present. if len(value) >= 2 { first, last := value[0], value[len(value)-1] if (first == '"' && last == '"') || (first == '\'' && last == '\'') { value = value[1 : len(value)-1] } } if _, alreadySet := os.LookupEnv(key); alreadySet { continue // real environment variables take precedence } if err := os.Setenv(key, value); err != nil { return fmt.Errorf("config: set %s: %w", key, err) } } return scanner.Err() }