// Package dotenv loads KEY=VALUE pairs from a .env file into the process // environment, so secrets can live in one file that is kept out of version // control instead of on command lines. // // Real environment variables always win over the file, so a deployment can // still override anything with its own environment. package dotenv import ( "bufio" "errors" "os" "strings" ) // Load reads the first of paths that exists and sets any variables it defines // that aren't already set. It returns the path used, or "" if none existed. func Load(paths ...string) (string, error) { for _, p := range paths { if p == "" { continue } f, err := os.Open(p) if errors.Is(err, os.ErrNotExist) { continue } if err != nil { return "", err } defer f.Close() sc := bufio.NewScanner(f) sc.Buffer(make([]byte, 64*1024), 1<<20) for sc.Scan() { k, v, ok := parseLine(sc.Text()) if !ok { continue } if _, set := os.LookupEnv(k); !set { os.Setenv(k, v) } } return p, sc.Err() } return "", nil } // ArgPath finds -env / --env= in args, so the file can be chosen // before the program's own flags are parsed (flags take their defaults from the environment). func ArgPath(args []string, def string) string { for i, a := range args { a = strings.TrimLeft(a, "-") switch { case a == "env" && i+1 < len(args): return args[i+1] case strings.HasPrefix(a, "env="): return a[len("env="):] } } return def } func parseLine(line string) (key, val string, ok bool) { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { return "", "", false } line = strings.TrimSpace(strings.TrimPrefix(line, "export ")) k, v, found := strings.Cut(line, "=") k = strings.TrimSpace(k) if !found || k == "" || strings.ContainsAny(k, " \t") { return "", "", false } v = strings.TrimSpace(v) switch { case len(v) >= 2 && v[0] == '"': if end := strings.LastIndexByte(v, '"'); end > 0 { v = strings.NewReplacer(`\n`, "\n", `\r`, "\r", `\t`, "\t", `\"`, `"`, `\\`, `\`).Replace(v[1:end]) } case len(v) >= 2 && v[0] == '\'': if end := strings.LastIndexByte(v, '\''); end > 0 { v = v[1:end] } default: if i := strings.Index(v, " #"); i >= 0 { // trailing comment v = strings.TrimSpace(v[:i]) } } return k, v, true }