package config import ( "bufio" "fmt" "io" "os" "strings" ) const ( dotenvExportPrefix = "export " dotenvCommentMarker = "#" dotenvInlineComment = " #" dotenvByteOrderMark = "\xef\xbb\xbf" dotenvMaxLineBytes = 1 << 20 dotenvAssignment = "=" dotenvDoubleQuote = '"' dotenvSingleQuote = '\'' dotenvEscapeBackslash = '\\' ) var dotenvDoubleQuoteEscapes = map[byte]byte{ 'n': '\n', 'r': '\r', 't': '\t', '"': '"', '\\': '\\', } func LoadDotenv(path string) error { file, err := os.Open(path) if err != nil { return err } defer func() { _ = file.Close() }() values, err := ParseDotenv(file) if err != nil { return fmt.Errorf("%s: %w", path, err) } for name, value := range values { if _, isSet := os.LookupEnv(name); !isSet { if err := os.Setenv(name, value); err != nil { return err } } } return nil } func ParseDotenv(reader io.Reader) (map[string]string, error) { scanner := bufio.NewScanner(reader) scanner.Buffer(make([]byte, 0, bufio.MaxScanTokenSize), dotenvMaxLineBytes) values := map[string]string{} for lineNumber := 1; scanner.Scan(); lineNumber++ { line := strings.TrimSpace(strings.TrimPrefix(scanner.Text(), dotenvByteOrderMark)) if line == "" || strings.HasPrefix(line, dotenvCommentMarker) { continue } name, value, err := parseDotenvAssignment(line) if err != nil { return nil, fmt.Errorf("line %d: %w", lineNumber, err) } values[name] = value } return values, scanner.Err() } func parseDotenvAssignment(line string) (string, string, error) { line = strings.TrimSpace(strings.TrimPrefix(line, dotenvExportPrefix)) name, raw, found := strings.Cut(line, dotenvAssignment) name = strings.TrimSpace(name) if !found || !isDotenvName(name) { return "", "", fmt.Errorf("expected NAME=value") } value, err := parseDotenvValue(strings.TrimSpace(raw)) return name, value, err } func isDotenvName(name string) bool { if name == "" { return false } for _, r := range name { isLetter := r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' isDigit := r >= '0' && r <= '9' if !isLetter && !isDigit && r != '_' && r != '.' { return false } } return true } func parseDotenvValue(raw string) (string, error) { if raw == "" { return "", nil } switch raw[0] { case dotenvDoubleQuote: return parseDoubleQuoted(raw) case dotenvSingleQuote: return parseSingleQuoted(raw) } if comment := strings.Index(raw, dotenvInlineComment); comment >= 0 { raw = raw[:comment] } return strings.TrimSpace(raw), nil } func parseSingleQuoted(raw string) (string, error) { end := strings.IndexByte(raw[1:], dotenvSingleQuote) if end < 0 { return "", fmt.Errorf("unterminated single quote") } return raw[1 : 1+end], nil } func parseDoubleQuoted(raw string) (string, error) { var value strings.Builder for i := 1; i < len(raw); i++ { switch raw[i] { case dotenvDoubleQuote: return value.String(), nil case dotenvEscapeBackslash: i++ if i >= len(raw) { return "", fmt.Errorf("unterminated double quote") } if unescaped, known := dotenvDoubleQuoteEscapes[raw[i]]; known { value.WriteByte(unescaped) } else { value.WriteByte(dotenvEscapeBackslash) value.WriteByte(raw[i]) } default: value.WriteByte(raw[i]) } } return "", fmt.Errorf("unterminated double quote") }