gotenv.go raw

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