1// Package gotenv provides functionality to dynamically load the environment variables 2package gotenv 3 4import ( 5 "bufio" 6 "fmt" 7 "io" 8 "os" 9 "regexp" 10 "strings" 11) 12 13const ( 14 // Pattern for detecting valid line format 15 linePattern = `\A\s*(?:export\s+)?([\w\.]+)(?:\s*=\s*|:\s+?)('(?:\'|[^'])*'|"(?:\"|[^"])*"|[^#\n]+)?\s*(?:\s*\#.*)?\z` 16 17 // Pattern for detecting valid variable within a value 18 variablePattern = `(\\)?(\$)(\{?([A-Z0-9_]+)?\}?)` 19) 20 21// Env holds key/value pair of valid environment variable 22type Env map[string]string 23 24/* 25Load is a function to load a file or multiple files and then export the valid variables into environment variables if they do not exist. 26When it's called with no argument, it will load `.env` file on the current path and set the environment variables. 27Otherwise, it will loop over the filenames parameter and set the proper environment variables. 28*/ 29func Load(filenames ...string) error { 30 return loadenv(false, filenames...) 31} 32 33/* 34OverLoad is a function to load a file or multiple files and then export and override the valid variables into environment variables. 35*/ 36func OverLoad(filenames ...string) error { 37 return loadenv(true, filenames...) 38} 39 40/* 41Must is wrapper function that will panic when supplied function returns an error. 42*/ 43func Must(fn func(filenames ...string) error, filenames ...string) { 44 if err := fn(filenames...); err != nil { 45 panic(err.Error()) 46 } 47} 48 49/* 50Apply is a function to load an io Reader then export the valid variables into environment variables if they do not exist. 51*/ 52func Apply(r io.Reader) error { 53 return parset(r, false) 54} 55 56/* 57OverApply is a function to load an io Reader then export and override the valid variables into environment variables. 58*/ 59func OverApply(r io.Reader) error { 60 return parset(r, true) 61} 62 63func loadenv(override bool, filenames ...string) error { 64 if len(filenames) == 0 { 65 filenames = []string{".env"} 66 } 67 68 for _, filename := range filenames { 69 f, err := os.Open(filename) 70 if err != nil { 71 return err 72 } 73 74 err = parset(f, override) 75 if err != nil { 76 return err 77 } 78 79 f.Close() 80 } 81 82 return nil 83} 84 85// parse and set :) 86func parset(r io.Reader, override bool) error { 87 env, err := StrictParse(r) 88 if err != nil { 89 return err 90 } 91 92 for key, val := range env { 93 setenv(key, val, override) 94 } 95 96 return nil 97} 98 99func setenv(key, val string, override bool) { 100 if override { 101 os.Setenv(key, val) 102 } else { 103 if _, present := os.LookupEnv(key); !present { 104 os.Setenv(key, val) 105 } 106 } 107} 108 109// Parse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables. 110// It expands the value of a variable from the environment variable but does not set the value to the environment itself. 111// This function is skipping any invalid lines and only processing the valid one. 112func Parse(r io.Reader) Env { 113 env, _ := StrictParse(r) 114 return env 115} 116 117// StrictParse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables. 118// It expands the value of a variable from the environment variable but does not set the value to the environment itself. 119// This function is returning an error if there are any invalid lines. 120func StrictParse(r io.Reader) (Env, error) { 121 env := make(Env) 122 scanner := bufio.NewScanner(r) 123 124 i := 1 125 bom := string([]byte{239, 187, 191}) 126 127 for scanner.Scan() { 128 line := scanner.Text() 129 130 if i == 1 { 131 line = strings.TrimPrefix(line, bom) 132 } 133 134 i++ 135 136 err := parseLine(line, env) 137 if err != nil { 138 return env, err 139 } 140 } 141 142 return env, nil 143} 144 145func parseLine(s string, env Env) error { 146 rl := regexp.MustCompile(linePattern) 147 rm := rl.FindStringSubmatch(s) 148 149 if len(rm) == 0 { 150 return checkFormat(s, env) 151 } 152 153 key := rm[1] 154 val := rm[2] 155 156 // determine if string has quote prefix 157 hdq := strings.HasPrefix(val, `"`) 158 159 // determine if string has single quote prefix 160 hsq := strings.HasPrefix(val, `'`) 161 162 // trim whitespace 163 val = strings.Trim(val, " ") 164 165 // remove quotes '' or "" 166 rq := regexp.MustCompile(`\A(['"])(.*)(['"])\z`) 167 val = rq.ReplaceAllString(val, "$2") 168 169 if hdq { 170 val = strings.Replace(val, `\n`, "\n", -1) 171 val = strings.Replace(val, `\r`, "\r", -1) 172 173 // Unescape all characters except $ so variables can be escaped properly 174 re := regexp.MustCompile(`\\([^$])`) 175 val = re.ReplaceAllString(val, "$1") 176 } 177 178 rv := regexp.MustCompile(variablePattern) 179 fv := func(s string) string { 180 return varReplacement(s, hsq, env) 181 } 182 183 val = rv.ReplaceAllStringFunc(val, fv) 184 val = parseVal(val, env) 185 186 env[key] = val 187 return nil 188} 189 190func parseExport(st string, env Env) error { 191 if strings.HasPrefix(st, "export") { 192 vs := strings.SplitN(st, " ", 2) 193 194 if len(vs) > 1 { 195 if _, ok := env[vs[1]]; !ok { 196 return fmt.Errorf("line `%s` has an unset variable", st) 197 } 198 } 199 } 200 201 return nil 202} 203 204func varReplacement(s string, hsq bool, env Env) string { 205 if strings.HasPrefix(s, "\\") { 206 return strings.TrimPrefix(s, "\\") 207 } 208 209 if hsq { 210 return s 211 } 212 213 sn := `(\$)(\{?([A-Z0-9_]+)\}?)` 214 rn := regexp.MustCompile(sn) 215 mn := rn.FindStringSubmatch(s) 216 217 if len(mn) == 0 { 218 return s 219 } 220 221 v := mn[3] 222 223 replace, ok := env[v] 224 if !ok { 225 replace = os.Getenv(v) 226 } 227 228 return replace 229} 230 231func checkFormat(s string, env Env) error { 232 st := strings.TrimSpace(s) 233 234 if (st == "") || strings.HasPrefix(st, "#") { 235 return nil 236 } 237 238 if err := parseExport(st, env); err != nil { 239 return err 240 } 241 242 return fmt.Errorf("line `%s` doesn't match format", s) 243} 244 245func parseVal(val string, env Env) string { 246 if strings.Contains(val, "=") { 247 if !(val == "\n" || val == "\r") { 248 kv := strings.Split(val, "\n") 249 250 if len(kv) == 1 { 251 kv = strings.Split(val, "\r") 252 } 253 254 if len(kv) > 1 { 255 val = kv[0] 256 257 for i := 1; i < len(kv); i++ { 258 parseLine(kv[i], env) 259 } 260 } 261 } 262 } 263 264 return val 265} 266