143 lines
3.3 KiB
Go
143 lines
3.3 KiB
Go
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")
|
|
}
|