1
// Package gotenv provides functionality to dynamically load the environment variables
18
// Pattern for detecting valid line format
19
linePattern = `\A\s*(?:export\s+)?([\w\.]+)(?:\s*=\s*|:\s+?)('(?:\'|[^'])*'|"(?:\"|[^"])*"|[^#\n]+)?\s*(?:\s*\#.*)?\z`
21
// Pattern for detecting valid variable within a value
22
variablePattern = `(\\)?(\$)(\{?([A-Z0-9_]+)?\}?)`
24
// Byte order mark character
28
// Env holds key/value pair of valid environment variable
29
type Env map[string]string
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...)
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...)
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 {
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)
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)
60
func loadenv(override bool, filenames ...string) error {
61
if len(filenames) == 0 {
62
filenames = []string{".env"}
65
for _, filename := range filenames {
66
f, err := os.Open(filename)
71
err = parset(f, override)
82
func parset(r io.Reader, override bool) error {
83
env, err := strictParse(r, override)
88
for key, val := range env {
89
setenv(key, val, override)
95
func setenv(key, val string, override bool) {
99
if _, present := os.LookupEnv(key); !present {
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)
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)
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)
129
return strictParse(f, false)
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)
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))
147
lines = append(lines, fmt.Sprintf(`%s=%q`, k, v))
151
return strings.Join(lines, "\n"), nil
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)
160
// ensure the path exists
161
if err := os.MkdirAll(filepath.Dir(filename), 0o775); err != nil {
164
// create or truncate the file
165
file, err := os.Create(filename)
170
_, err = file.WriteString(content + "\n")
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
185
idx := bytes.IndexAny(data, "\r\n")
187
case atEOF && idx < 0:
188
return len(data), data, bufio.ErrFinalToken
197
if len(data) > eol && data[eol-1] == '\r' && data[eol] == '\n' {
201
return eol, data[:idx], nil
204
func strictParse(r io.Reader, override bool) (Env, error) {
206
scanner := bufio.NewScanner(r)
207
scanner.Split(splitLines)
212
line := strings.TrimSpace(scanner.Text())
215
line = strings.TrimPrefix(line, bom)
219
if line == "" || line[0] == '#' {
224
// look for the delimiter character
225
idx := strings.Index(line, "=")
227
idx = strings.Index(line, ":")
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] == '\'' {
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] != '\\' {
241
// look for the closing quote character
242
for quote != "" && scanner.Scan() {
245
idx := strings.LastIndex(l, quote)
246
if idx > 0 && l[idx-1] == '\\' {
247
// foud a matching quote character but it's escaped
251
// foud a matching quote
257
return env, fmt.Errorf("missing quotes")
260
err := parseLine(line, env, override)
270
lineRgx = regexp.MustCompile(linePattern)
271
unescapeRgx = regexp.MustCompile(`\\([^$])`)
272
varRgx = regexp.MustCompile(variablePattern)
275
func parseLine(s string, env Env, override bool) error {
276
rm := lineRgx.FindStringSubmatch(s)
279
return checkFormat(s, env)
282
key := strings.TrimSpace(rm[1])
283
val := strings.TrimSpace(rm[2])
287
// check if the value is quoted
288
if l := len(val); l >= 2 {
291
hdq = val[0] == '"' && val[l] == '"'
293
hsq = val[0] == '\'' && val[l] == '\''
295
// remove quotes '' or ""
302
val = strings.ReplaceAll(val, `\n`, "\n")
303
val = strings.ReplaceAll(val, `\r`, "\r")
305
// Unescape all characters except $ so variables can be escaped properly
306
val = unescapeRgx.ReplaceAllString(val, "$1")
310
fv := func(s string) string {
311
return varReplacement(s, hsq, env, override)
313
val = varRgx.ReplaceAllStringFunc(val, fv)
320
func parseExport(st string, env Env) error {
321
if strings.HasPrefix(st, "export") {
322
vs := strings.SplitN(st, " ", 2)
325
if _, ok := env[vs[1]]; !ok {
326
return fmt.Errorf("line `%s` has an unset variable", st)
334
var varNameRgx = regexp.MustCompile(`(\$)(\{?([A-Z0-9_]+)\}?)`)
336
func varReplacement(s string, hsq bool, env Env, override bool) string {
342
// the dollar sign is escaped
350
mn := varNameRgx.FindStringSubmatch(s)
358
if replace, ok := os.LookupEnv(v); ok && !override {
362
if replace, ok := env[v]; ok {
369
func checkFormat(s string, env Env) error {
370
st := strings.TrimSpace(s)
372
if st == "" || st[0] == '#' {
376
if err := parseExport(st, env); err != nil {
380
return fmt.Errorf("line `%s` doesn't match format", s)