model, tunnel, wg, authtoken, netguard, docker and dotenv, split out of the original single repository so Hammerhead, Blacktip and Bamboo can live in their own repos and import them. README extended; LICENSE unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
90 lines
2.3 KiB
Go
90 lines
2.3 KiB
Go
// 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 <path> / --env=<path> 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
|
|
}
|