podman

Форк
0
381 строка · 9.1 Кб
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
}
382

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.