// 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_]+)?\}?)` ) // 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) 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) 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) { env := make(Env) scanner := bufio.NewScanner(r) i := 1 bom := string([]byte{239, 187, 191}) for scanner.Scan() { line := scanner.Text() if i == 1 { line = strings.TrimPrefix(line, bom) } i++ err := parseLine(line, env) if err != nil { return env, err } } return env, nil } func parseLine(s string, env Env) error { rl := regexp.MustCompile(linePattern) rm := rl.FindStringSubmatch(s) if len(rm) == 0 { return checkFormat(s, env) } key := rm[1] val := rm[2] // determine if string has quote prefix hdq := strings.HasPrefix(val, `"`) // determine if string has single quote prefix hsq := strings.HasPrefix(val, `'`) // trim whitespace val = strings.Trim(val, " ") // remove quotes '' or "" rq := regexp.MustCompile(`\A(['"])(.*)(['"])\z`) val = rq.ReplaceAllString(val, "$2") if hdq { val = strings.Replace(val, `\n`, "\n", -1) val = strings.Replace(val, `\r`, "\r", -1) // Unescape all characters except $ so variables can be escaped properly re := regexp.MustCompile(`\\([^$])`) val = re.ReplaceAllString(val, "$1") } rv := regexp.MustCompile(variablePattern) fv := func(s string) string { return varReplacement(s, hsq, env) } val = rv.ReplaceAllStringFunc(val, fv) val = parseVal(val, env) 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 } func varReplacement(s string, hsq bool, env Env) string { if strings.HasPrefix(s, "\\") { return strings.TrimPrefix(s, "\\") } if hsq { return s } sn := `(\$)(\{?([A-Z0-9_]+)\}?)` rn := regexp.MustCompile(sn) mn := rn.FindStringSubmatch(s) if len(mn) == 0 { return s } v := mn[3] 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) string { if strings.Contains(val, "=") { if !(val == "\n" || val == "\r") { kv := strings.Split(val, "\n") 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) } } } } return val }