gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

gotenv.go (9352B)


      1 // Package gotenv provides functionality to dynamically load the environment variables
      2 package gotenv
      3 
      4 import (
      5 	"bufio"
      6 	"bytes"
      7 	"fmt"
      8 	"io"
      9 	"os"
     10 	"path/filepath"
     11 	"regexp"
     12 	"sort"
     13 	"strconv"
     14 	"strings"
     15 )
     16 
     17 const (
     18 	// Pattern for detecting valid line format
     19 	linePattern = `\A\s*(?:export\s+)?([\w\.]+)(?:\s*=\s*|:\s+?)('(?:\'|[^'])*'|"(?:\"|[^"])*"|[^#\n]+)?\s*(?:\s*\#.*)?\z`
     20 
     21 	// Pattern for detecting valid variable within a value
     22 	variablePattern = `(\\)?(\$)(\{?([A-Z0-9_]+)?\}?)`
     23 
     24 	// Byte order mark character
     25 	bom = "\xef\xbb\xbf"
     26 )
     27 
     28 // Env holds key/value pair of valid environment variable
     29 type Env map[string]string
     30 
     31 // 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.
     32 // When it's called with no argument, it will load `.env` file on the current path and set the environment variables.
     33 // Otherwise, it will loop over the filenames parameter and set the proper environment variables.
     34 func Load(filenames ...string) error {
     35 	return loadenv(false, filenames...)
     36 }
     37 
     38 // OverLoad is a function to load a file or multiple files and then export and override the valid variables into environment variables.
     39 func OverLoad(filenames ...string) error {
     40 	return loadenv(true, filenames...)
     41 }
     42 
     43 // Must is wrapper function that will panic when supplied function returns an error.
     44 func Must(fn func(filenames ...string) error, filenames ...string) {
     45 	if err := fn(filenames...); err != nil {
     46 		panic(err.Error())
     47 	}
     48 }
     49 
     50 // Apply is a function to load an io Reader then export the valid variables into environment variables if they do not exist.
     51 func Apply(r io.Reader) error {
     52 	return parset(r, false)
     53 }
     54 
     55 // OverApply is a function to load an io Reader then export and override the valid variables into environment variables.
     56 func OverApply(r io.Reader) error {
     57 	return parset(r, true)
     58 }
     59 
     60 func loadenv(override bool, filenames ...string) error {
     61 	if len(filenames) == 0 {
     62 		filenames = []string{".env"}
     63 	}
     64 
     65 	for _, filename := range filenames {
     66 		f, err := os.Open(filename)
     67 		if err != nil {
     68 			return err
     69 		}
     70 
     71 		err = parset(f, override)
     72 		f.Close()
     73 		if err != nil {
     74 			return err
     75 		}
     76 	}
     77 
     78 	return nil
     79 }
     80 
     81 // parse and set :)
     82 func parset(r io.Reader, override bool) error {
     83 	env, err := strictParse(r, override)
     84 	if err != nil {
     85 		return err
     86 	}
     87 
     88 	for key, val := range env {
     89 		setenv(key, val, override)
     90 	}
     91 
     92 	return nil
     93 }
     94 
     95 func setenv(key, val string, override bool) {
     96 	if override {
     97 		os.Setenv(key, val)
     98 	} else {
     99 		if _, present := os.LookupEnv(key); !present {
    100 			os.Setenv(key, val)
    101 		}
    102 	}
    103 }
    104 
    105 // Parse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables.
    106 // It expands the value of a variable from the environment variable but does not set the value to the environment itself.
    107 // This function is skipping any invalid lines and only processing the valid one.
    108 func Parse(r io.Reader) Env {
    109 	env, _ := strictParse(r, false)
    110 	return env
    111 }
    112 
    113 // StrictParse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables.
    114 // It expands the value of a variable from the environment variable but does not set the value to the environment itself.
    115 // This function is returning an error if there are any invalid lines.
    116 func StrictParse(r io.Reader) (Env, error) {
    117 	return strictParse(r, false)
    118 }
    119 
    120 // Read is a function to parse a file line by line and returns the valid Env key/value pair of valid variables.
    121 // It expands the value of a variable from the environment variable but does not set the value to the environment itself.
    122 // This function is skipping any invalid lines and only processing the valid one.
    123 func Read(filename string) (Env, error) {
    124 	f, err := os.Open(filename)
    125 	if err != nil {
    126 		return nil, err
    127 	}
    128 	defer f.Close()
    129 	return strictParse(f, false)
    130 }
    131 
    132 // Unmarshal reads a string line by line and returns the valid Env key/value pair of valid variables.
    133 // It expands the value of a variable from the environment variable but does not set the value to the environment itself.
    134 // This function is returning an error if there are any invalid lines.
    135 func Unmarshal(str string) (Env, error) {
    136 	return strictParse(strings.NewReader(str), false)
    137 }
    138 
    139 // Marshal outputs the given environment as a env file.
    140 // Variables will be sorted by name.
    141 func Marshal(env Env) (string, error) {
    142 	lines := make([]string, 0, len(env))
    143 	for k, v := range env {
    144 		if d, err := strconv.Atoi(v); err == nil {
    145 			lines = append(lines, fmt.Sprintf(`%s=%d`, k, d))
    146 		} else {
    147 			lines = append(lines, fmt.Sprintf(`%s=%q`, k, v))
    148 		}
    149 	}
    150 	sort.Strings(lines)
    151 	return strings.Join(lines, "\n"), nil
    152 }
    153 
    154 // Write serializes the given environment and writes it to a file
    155 func Write(env Env, filename string) error {
    156 	content, err := Marshal(env)
    157 	if err != nil {
    158 		return err
    159 	}
    160 	// ensure the path exists
    161 	if err := os.MkdirAll(filepath.Dir(filename), 0o775); err != nil {
    162 		return err
    163 	}
    164 	// create or truncate the file
    165 	file, err := os.Create(filename)
    166 	if err != nil {
    167 		return err
    168 	}
    169 	defer file.Close()
    170 	_, err = file.WriteString(content + "\n")
    171 	if err != nil {
    172 		return err
    173 	}
    174 
    175 	return file.Sync()
    176 }
    177 
    178 // splitLines is a valid SplitFunc for a bufio.Scanner. It will split lines on CR ('\r'), LF ('\n') or CRLF (any of the three sequences).
    179 // If a CR is immediately followed by a LF, it is treated as a CRLF (one single line break).
    180 func splitLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
    181 	if atEOF && len(data) == 0 {
    182 		return 0, nil, bufio.ErrFinalToken
    183 	}
    184 
    185 	idx := bytes.IndexAny(data, "\r\n")
    186 	switch {
    187 	case atEOF && idx < 0:
    188 		return len(data), data, bufio.ErrFinalToken
    189 
    190 	case idx < 0:
    191 		return 0, nil, nil
    192 	}
    193 
    194 	// consume CR or LF
    195 	eol := idx + 1
    196 	// detect CRLF
    197 	if len(data) > eol && data[eol-1] == '\r' && data[eol] == '\n' {
    198 		eol++
    199 	}
    200 
    201 	return eol, data[:idx], nil
    202 }
    203 
    204 func strictParse(r io.Reader, override bool) (Env, error) {
    205 	env := make(Env)
    206 	scanner := bufio.NewScanner(r)
    207 	scanner.Split(splitLines)
    208 
    209 	firstLine := true
    210 
    211 	for scanner.Scan() {
    212 		line := strings.TrimSpace(scanner.Text())
    213 
    214 		if firstLine {
    215 			line = strings.TrimPrefix(line, bom)
    216 			firstLine = false
    217 		}
    218 
    219 		if line == "" || line[0] == '#' {
    220 			continue
    221 		}
    222 
    223 		quote := ""
    224 		// look for the delimiter character
    225 		idx := strings.Index(line, "=")
    226 		if idx == -1 {
    227 			idx = strings.Index(line, ":")
    228 		}
    229 		// look for a quote character
    230 		if idx > 0 && idx < len(line)-1 {
    231 			val := strings.TrimSpace(line[idx+1:])
    232 			if val[0] == '"' || val[0] == '\'' {
    233 				quote = val[:1]
    234 				// look for the closing quote character within the same line
    235 				idx = strings.LastIndex(strings.TrimSpace(val[1:]), quote)
    236 				if idx >= 0 && val[idx] != '\\' {
    237 					quote = ""
    238 				}
    239 			}
    240 		}
    241 		// look for the closing quote character
    242 		for quote != "" && scanner.Scan() {
    243 			l := scanner.Text()
    244 			line += "\n" + l
    245 			idx := strings.LastIndex(l, quote)
    246 			if idx > 0 && l[idx-1] == '\\' {
    247 				// foud a matching quote character but it's escaped
    248 				continue
    249 			}
    250 			if idx >= 0 {
    251 				// foud a matching quote
    252 				quote = ""
    253 			}
    254 		}
    255 
    256 		if quote != "" {
    257 			return env, fmt.Errorf("missing quotes")
    258 		}
    259 
    260 		err := parseLine(line, env, override)
    261 		if err != nil {
    262 			return env, err
    263 		}
    264 	}
    265 
    266 	return env, nil
    267 }
    268 
    269 var (
    270 	lineRgx     = regexp.MustCompile(linePattern)
    271 	unescapeRgx = regexp.MustCompile(`\\([^$])`)
    272 	varRgx      = regexp.MustCompile(variablePattern)
    273 )
    274 
    275 func parseLine(s string, env Env, override bool) error {
    276 	rm := lineRgx.FindStringSubmatch(s)
    277 
    278 	if len(rm) == 0 {
    279 		return checkFormat(s, env)
    280 	}
    281 
    282 	key := strings.TrimSpace(rm[1])
    283 	val := strings.TrimSpace(rm[2])
    284 
    285 	var hsq, hdq bool
    286 
    287 	// check if the value is quoted
    288 	if l := len(val); l >= 2 {
    289 		l -= 1
    290 		// has double quotes
    291 		hdq = val[0] == '"' && val[l] == '"'
    292 		// has single quotes
    293 		hsq = val[0] == '\'' && val[l] == '\''
    294 
    295 		// remove quotes '' or ""
    296 		if hsq || hdq {
    297 			val = val[1:l]
    298 		}
    299 	}
    300 
    301 	if hdq {
    302 		val = strings.ReplaceAll(val, `\n`, "\n")
    303 		val = strings.ReplaceAll(val, `\r`, "\r")
    304 
    305 		// Unescape all characters except $ so variables can be escaped properly
    306 		val = unescapeRgx.ReplaceAllString(val, "$1")
    307 	}
    308 
    309 	if !hsq {
    310 		fv := func(s string) string {
    311 			return varReplacement(s, hsq, env, override)
    312 		}
    313 		val = varRgx.ReplaceAllStringFunc(val, fv)
    314 	}
    315 
    316 	env[key] = val
    317 	return nil
    318 }
    319 
    320 func parseExport(st string, env Env) error {
    321 	if strings.HasPrefix(st, "export") {
    322 		vs := strings.SplitN(st, " ", 2)
    323 
    324 		if len(vs) > 1 {
    325 			if _, ok := env[vs[1]]; !ok {
    326 				return fmt.Errorf("line `%s` has an unset variable", st)
    327 			}
    328 		}
    329 	}
    330 
    331 	return nil
    332 }
    333 
    334 var varNameRgx = regexp.MustCompile(`(\$)(\{?([A-Z0-9_]+)\}?)`)
    335 
    336 func varReplacement(s string, hsq bool, env Env, override bool) string {
    337 	if s == "" {
    338 		return s
    339 	}
    340 
    341 	if s[0] == '\\' {
    342 		// the dollar sign is escaped
    343 		return s[1:]
    344 	}
    345 
    346 	if hsq {
    347 		return s
    348 	}
    349 
    350 	mn := varNameRgx.FindStringSubmatch(s)
    351 
    352 	if len(mn) == 0 {
    353 		return s
    354 	}
    355 
    356 	v := mn[3]
    357 
    358 	if replace, ok := os.LookupEnv(v); ok && !override {
    359 		return replace
    360 	}
    361 
    362 	if replace, ok := env[v]; ok {
    363 		return replace
    364 	}
    365 
    366 	return os.Getenv(v)
    367 }
    368 
    369 func checkFormat(s string, env Env) error {
    370 	st := strings.TrimSpace(s)
    371 
    372 	if st == "" || st[0] == '#' {
    373 		return nil
    374 	}
    375 
    376 	if err := parseExport(st, env); err != nil {
    377 		return err
    378 	}
    379 
    380 	return fmt.Errorf("line `%s` doesn't match format", s)
    381 }