...

Source file src/github.com/subosito/gotenv/gotenv.go

Documentation: github.com/subosito/gotenv

     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  

View as plain text