vendor/github.com/subosito/gotenv/gotenv.go
changeset 260 445e01aede7e
parent 251 1c52a0eeb952
child 265 05c40b36d3b2
--- a/vendor/github.com/subosito/gotenv/gotenv.go	Tue Aug 23 22:33:28 2022 +0200
+++ b/vendor/github.com/subosito/gotenv/gotenv.go	Tue Aug 23 22:39:43 2022 +0200
@@ -6,7 +6,10 @@
 	"fmt"
 	"io"
 	"os"
+	"path/filepath"
 	"regexp"
+	"sort"
+	"strconv"
 	"strings"
 )
 
@@ -16,46 +19,39 @@
 
 	// 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.
-*/
+// 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.
-*/
+// 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.
-*/
+// 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.
-*/
+// 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.
-*/
+// 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)
 }
@@ -72,11 +68,10 @@
 		}
 
 		err = parset(f, override)
+		f.Close()
 		if err != nil {
 			return err
 		}
-
-		f.Close()
 	}
 
 	return nil
@@ -84,7 +79,7 @@
 
 // parse and set :)
 func parset(r io.Reader, override bool) error {
-	env, err := StrictParse(r)
+	env, err := strictParse(r, override)
 	if err != nil {
 		return err
 	}
@@ -110,7 +105,7 @@
 // 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)
+	env, _ := strictParse(r, false)
 	return env
 }
 
@@ -118,22 +113,123 @@
 // 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)
+}
+
+// Read is a function to parse a file line by line 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 Read(filename string) (Env, error) {
+	f, err := os.Open(filename)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	return strictParse(f, false)
+}
+
+// Unmarshal reads a string line by line 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 Unmarshal(str string) (Env, error) {
+	return strictParse(strings.NewReader(str), false)
+}
+
+// Marshal outputs the given environment as a env file.
+// Variables will be sorted by name.
+func Marshal(env Env) (string, error) {
+	lines := make([]string, 0, len(env))
+	for k, v := range env {
+		if d, err := strconv.Atoi(v); err == nil {
+			lines = append(lines, fmt.Sprintf(`%s=%d`, k, d))
+		} else {
+			lines = append(lines, fmt.Sprintf(`%s=%q`, k, v))
+		}
+	}
+	sort.Strings(lines)
+	return strings.Join(lines, "\n"), nil
+}
+
+// Write serializes the given environment and writes it to a file
+func Write(env Env, filename string) error {
+	content, err := Marshal(env)
+	if err != nil {
+		return err
+	}
+	// ensure the path exists
+	if err := os.MkdirAll(filepath.Dir(filename), 0o775); err != nil {
+		return err
+	}
+	// create or truncate the file
+	file, err := os.Create(filename)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+	_, err = file.WriteString(content + "\n")
+	if err != nil {
+		return err
+	}
+
+	return file.Sync()
+}
+
+func strictParse(r io.Reader, override bool) (Env, error) {
 	env := make(Env)
 	scanner := bufio.NewScanner(r)
 
-	i := 1
-	bom := string([]byte{239, 187, 191})
+	firstLine := true
 
 	for scanner.Scan() {
-		line := scanner.Text()
+		line := strings.TrimSpace(scanner.Text())
 
-		if i == 1 {
+		if firstLine {
 			line = strings.TrimPrefix(line, bom)
+			firstLine = false
+		}
+
+		if line == "" || line[0] == '#' {
+			continue
 		}
 
-		i++
+		quote := ""
+		// look for the delimiter character
+		idx := strings.Index(line, "=")
+		if idx == -1 {
+			idx = strings.Index(line, ":")
+		}
+		// look for a quote character
+		if idx > 0 && idx < len(line)-1 {
+			val := strings.TrimSpace(line[idx+1:])
+			if val[0] == '"' || val[0] == '\'' {
+				quote = val[:1]
+				// look for the closing quote character within the same line
+				idx = strings.LastIndex(strings.TrimSpace(val[1:]), quote)
+				if idx >= 0 && val[idx] != '\\' {
+					quote = ""
+				}
+			}
+		}
+		// look for the closing quote character
+		for quote != "" && scanner.Scan() {
+			l := scanner.Text()
+			line += "\n" + l
+			idx := strings.LastIndex(l, quote)
+			if idx > 0 && l[idx-1] == '\\' {
+				// foud a matching quote character but it's escaped
+				continue
+			}
+			if idx >= 0 {
+				// foud a matching quote
+				quote = ""
+			}
+		}
 
-		err := parseLine(line, env)
+		if quote != "" {
+			return env, fmt.Errorf("missing quotes")
+		}
+
+		err := parseLine(line, env, override)
 		if err != nil {
 			return env, err
 		}
@@ -142,47 +238,54 @@
 	return env, nil
 }
 
-func parseLine(s string, env Env) error {
-	rl := regexp.MustCompile(linePattern)
-	rm := rl.FindStringSubmatch(s)
+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]
+	key := strings.TrimSpace(rm[1])
+	val := strings.TrimSpace(rm[2])
 
-	// determine if string has quote prefix
-	hdq := strings.HasPrefix(val, `"`)
+	var hsq, hdq bool
 
-	// determine if string has single quote prefix
-	hsq := strings.HasPrefix(val, `'`)
+	// check if the value is quoted
+	if l := len(val); l >= 2 {
+		l -= 1
+		// has double quotes
+		hdq = val[0] == '"' && val[l] == '"'
+		// has single quotes
+		hsq = val[0] == '\'' && val[l] == '\''
 
-	// trim whitespace
-	val = strings.Trim(val, " ")
-
-	// remove quotes '' or ""
-	rq := regexp.MustCompile(`\A(['"])(.*)(['"])\z`)
-	val = rq.ReplaceAllString(val, "$2")
+		// remove quotes '' or ""
+		if hsq || hdq {
+			val = val[1:l]
+		}
+	}
 
 	if hdq {
-		val = strings.Replace(val, `\n`, "\n", -1)
-		val = strings.Replace(val, `\r`, "\r", -1)
+		val = strings.ReplaceAll(val, `\n`, "\n")
+		val = strings.ReplaceAll(val, `\r`, "\r")
 
 		// Unescape all characters except $ so variables can be escaped properly
-		re := regexp.MustCompile(`\\([^$])`)
-		val = re.ReplaceAllString(val, "$1")
+		val = unescapeRgx.ReplaceAllString(val, "$1")
 	}
 
-	rv := regexp.MustCompile(variablePattern)
-	fv := func(s string) string {
-		return varReplacement(s, hsq, env)
+	if !hsq {
+		fv := func(s string) string {
+			return varReplacement(s, hsq, env, override)
+		}
+		val = varRgx.ReplaceAllStringFunc(val, fv)
+		val = parseVal(val, env, hdq, override)
 	}
 
-	val = rv.ReplaceAllStringFunc(val, fv)
-	val = parseVal(val, env)
-
 	env[key] = val
 	return nil
 }
@@ -201,18 +304,23 @@
 	return nil
 }
 
-func varReplacement(s string, hsq bool, env Env) string {
-	if strings.HasPrefix(s, "\\") {
-		return strings.TrimPrefix(s, "\\")
+var varNameRgx = regexp.MustCompile(`(\$)(\{?([A-Z0-9_]+)\}?)`)
+
+func varReplacement(s string, hsq bool, env Env, override bool) string {
+	if s == "" {
+		return s
+	}
+
+	if s[0] == '\\' {
+		// the dollar sign is escaped
+		return s[1:]
 	}
 
 	if hsq {
 		return s
 	}
 
-	sn := `(\$)(\{?([A-Z0-9_]+)\}?)`
-	rn := regexp.MustCompile(sn)
-	mn := rn.FindStringSubmatch(s)
+	mn := varNameRgx.FindStringSubmatch(s)
 
 	if len(mn) == 0 {
 		return s
@@ -220,18 +328,21 @@
 
 	v := mn[3]
 
-	replace, ok := env[v]
-	if !ok {
-		replace = os.Getenv(v)
+	if replace, ok := os.LookupEnv(v); ok && !override {
+		return replace
 	}
 
-	return replace
+	if replace, ok := env[v]; ok {
+		return replace
+	}
+
+	return os.Getenv(v)
 }
 
 func checkFormat(s string, env Env) error {
 	st := strings.TrimSpace(s)
 
-	if (st == "") || strings.HasPrefix(st, "#") {
+	if st == "" || st[0] == '#' {
 		return nil
 	}
 
@@ -242,21 +353,14 @@
 	return fmt.Errorf("line `%s` doesn't match format", s)
 }
 
-func parseVal(val string, env Env) string {
-	if strings.Contains(val, "=") {
-		if !(val == "\n" || val == "\r") {
-			kv := strings.Split(val, "\n")
+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 {
-				kv = strings.Split(val, "\r")
-			}
-
-			if len(kv) > 1 {
-				val = kv[0]
-
-				for i := 1; i < len(kv); i++ {
-					parseLine(kv[i], env)
-				}
+		if len(kv) > 1 {
+			val = kv[0]
+			for _, l := range kv[1:] {
+				_ = parseLine(l, env, override)
 			}
 		}
 	}