ziggs/vendor/github.com/subosito/gotenv/gotenv.go

309 lines
6.7 KiB
Go

// Package gotenv provides functionality to dynamically load the environment variables
package gotenv
import (
"bufio"
"fmt"
"io"
"os"
"regexp"
"strings"
)
const (
// Pattern for detecting valid line format
linePattern = `\A\s*(?:export\s+)?([\w\.]+)(?:\s*=\s*|:\s+?)('(?:\'|[^'])*'|"(?:\"|[^"])*"|[^#\n]+)?\s*(?:\s*\#.*)?\z`
// Pattern for detecting valid variable within a value
variablePattern = `(\\)?(\$)(\{?([A-Z0-9_]+)?\}?)`
// Byte order mark character
bom = "\xef\xbb\xbf"
)
// Env holds key/value pair of valid environment variable
type Env map[string]string
/*
Load is a function to load a file or multiple files and then export the valid variables into environment variables if they do not exist.
When it's called with no argument, it will load `.env` file on the current path and set the environment variables.
Otherwise, it will loop over the filenames parameter and set the proper environment variables.
*/
func Load(filenames ...string) error {
return loadenv(false, filenames...)
}
/*
OverLoad is a function to load a file or multiple files and then export and override the valid variables into environment variables.
*/
func OverLoad(filenames ...string) error {
return loadenv(true, filenames...)
}
/*
Must is wrapper function that will panic when supplied function returns an error.
*/
func Must(fn func(filenames ...string) error, filenames ...string) {
if err := fn(filenames...); err != nil {
panic(err.Error())
}
}
/*
Apply is a function to load an io Reader then export the valid variables into environment variables if they do not exist.
*/
func Apply(r io.Reader) error {
return parset(r, false)
}
/*
OverApply is a function to load an io Reader then export and override the valid variables into environment variables.
*/
func OverApply(r io.Reader) error {
return parset(r, true)
}
func loadenv(override bool, filenames ...string) error {
if len(filenames) == 0 {
filenames = []string{".env"}
}
for _, filename := range filenames {
f, err := os.Open(filename)
if err != nil {
return err
}
err = parset(f, override)
if err != nil {
return err
}
f.Close()
}
return nil
}
// parse and set :)
func parset(r io.Reader, override bool) error {
env, err := strictParse(r, override)
if err != nil {
return err
}
for key, val := range env {
setenv(key, val, override)
}
return nil
}
func setenv(key, val string, override bool) {
if override {
os.Setenv(key, val)
} else {
if _, present := os.LookupEnv(key); !present {
os.Setenv(key, val)
}
}
}
// Parse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables.
// It expands the value of a variable from the environment variable but does not set the value to the environment itself.
// This function is skipping any invalid lines and only processing the valid one.
func Parse(r io.Reader) Env {
env, _ := strictParse(r, false)
return env
}
// StrictParse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables.
// It expands the value of a variable from the environment variable but does not set the value to the environment itself.
// This function is returning an error if there are any invalid lines.
func StrictParse(r io.Reader) (Env, error) {
return strictParse(r, false)
}
func strictParse(r io.Reader, override bool) (Env, error) {
env := make(Env)
scanner := bufio.NewScanner(r)
firstLine := true
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if firstLine {
line = strings.TrimPrefix(line, bom)
firstLine = false
}
if line == "" || line[0] == '#' {
continue
}
quote := ""
idx := strings.Index(line, "=")
if idx == -1 {
idx = strings.Index(line, ":")
}
if idx > 0 && idx < len(line)-1 {
val := strings.TrimSpace(line[idx+1:])
if val[0] == '"' || val[0] == '\'' {
quote = val[:1]
idx = strings.LastIndex(strings.TrimSpace(val[1:]), quote)
if idx >= 0 && val[idx] != '\\' {
quote = ""
}
}
}
for quote != "" && scanner.Scan() {
l := scanner.Text()
line += "\n" + l
idx := strings.LastIndex(l, quote)
if idx > 0 && l[idx-1] == '\\' {
continue
}
if idx >= 0 {
quote = ""
}
}
if quote != "" {
return env, fmt.Errorf("missing quotes")
}
err := parseLine(line, env, override)
if err != nil {
return env, err
}
}
return env, nil
}
var (
lineRgx = regexp.MustCompile(linePattern)
unescapeRgx = regexp.MustCompile(`\\([^$])`)
varRgx = regexp.MustCompile(variablePattern)
)
func parseLine(s string, env Env, override bool) error {
rm := lineRgx.FindStringSubmatch(s)
if len(rm) == 0 {
return checkFormat(s, env)
}
key := rm[1]
val := rm[2]
// trim whitespace
val = strings.TrimSpace(val)
// determine if string has quote prefix
hdq := strings.HasPrefix(val, `"`)
// determine if string has single quote prefix
hsq := strings.HasPrefix(val, `'`)
// remove quotes '' or ""
if l := len(val); (hsq || hdq) && l >= 2 {
val = val[1 : l-1]
}
if hdq {
val = strings.ReplaceAll(val, `\n`, "\n")
val = strings.ReplaceAll(val, `\r`, "\r")
// Unescape all characters except $ so variables can be escaped properly
val = unescapeRgx.ReplaceAllString(val, "$1")
}
fv := func(s string) string {
return varReplacement(s, hsq, env, override)
}
if !hsq {
val = varRgx.ReplaceAllStringFunc(val, fv)
val = parseVal(val, env, hdq, override)
}
env[key] = val
return nil
}
func parseExport(st string, env Env) error {
if strings.HasPrefix(st, "export") {
vs := strings.SplitN(st, " ", 2)
if len(vs) > 1 {
if _, ok := env[vs[1]]; !ok {
return fmt.Errorf("line `%s` has an unset variable", st)
}
}
}
return nil
}
var varNameRgx = regexp.MustCompile(`(\$)(\{?([A-Z0-9_]+)\}?)`)
func varReplacement(s string, hsq bool, env Env, override bool) string {
if strings.HasPrefix(s, "\\") {
return strings.TrimPrefix(s, "\\")
}
if hsq {
return s
}
mn := varNameRgx.FindStringSubmatch(s)
if len(mn) == 0 {
return s
}
v := mn[3]
if replace, ok := os.LookupEnv(v); ok && !override {
return replace
}
replace, ok := env[v]
if !ok {
replace = os.Getenv(v)
}
return replace
}
func checkFormat(s string, env Env) error {
st := strings.TrimSpace(s)
if (st == "") || strings.HasPrefix(st, "#") {
return nil
}
if err := parseExport(st, env); err != nil {
return err
}
return fmt.Errorf("line `%s` doesn't match format", s)
}
func parseVal(val string, env Env, ignoreNewlines bool, override bool) string {
if strings.Contains(val, "=") && !ignoreNewlines {
kv := strings.Split(val, "\r")
if len(kv) > 1 {
val = kv[0]
for _, l := range kv[1:] {
_ = parseLine(l, env, override)
}
}
}
return val
}