// Package env provides a minimal, dependency-free .env file loader. // // It reads KEY=VAL pairs from a file and sets them into the process // environment using os.Setenv, but only for variables that are NOT already // set. This gives the real environment (system env / shell) priority over // values in the .env file. package env import ( "bufio" "fmt" "os" "strings" ) // DefaultPath is the file LoadEnvDefault reads when no path is given. const DefaultPath = ".env" // LoadEnvDefault loads environment variables from the default ".env" file // in the current working directory. func LoadEnvDefault() error { return LoadEnv(DefaultPath) } // LoadEnv reads the file at path line by line and sets each KEY=VALUE pair // into the process environment. // // Rules: // - Blank lines and lines starting with '#' (comments) are ignored. // - Values may be wrapped in single or double quotes, which are stripped. // - A variable already present in the environment is never overridden // (system env wins; .env is the lowest priority). func LoadEnv(path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } key, value, ok := strings.Cut(line, "=") if !ok { // Not KEY=VAL format; skip silently. continue } key = strings.TrimSpace(key) if key == "" { continue } value = strings.TrimSpace(value) value = unquote(value) if _, exists := os.LookupEnv(key); exists { // System environment wins over .env. continue } if err := os.Setenv(key, value); err != nil { return fmt.Errorf("failed to set env var %q: %w", key, err) } } if err := scanner.Err(); err != nil { return fmt.Errorf("failed to read %s: %w", path, err) } return nil } // unquote strips a single matching pair of surrounding single or double // quotes from value, if present. Any other value is returned unchanged. func unquote(value string) string { if len(value) >= 2 { first, last := value[0], value[len(value)-1] if (first == '"' && last == '"') || (first == '\'' && last == '\'') { return value[1 : len(value)-1] } } return value }