Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
kardolus
GitHub Repository: kardolus/chatgpt-cli
Path: blob/main/vendor/github.com/subosito/gotenv/gotenv.go
2875 views
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
411