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 }