Path: blob/main/vendor/github.com/subosito/gotenv/gotenv.go
2875 views
// Package gotenv provides functionality to dynamically load the environment variables1package gotenv23import (4"bufio"5"bytes"6"fmt"7"io"8"os"9"path/filepath"10"regexp"11"sort"12"strconv"13"strings"1415"golang.org/x/text/encoding/unicode"16"golang.org/x/text/transform"17)1819const (20// Pattern for detecting valid line format21linePattern = `\A\s*(?:export\s+)?([\w\.]+)(?:\s*=\s*|:\s+?)('(?:\'|[^'])*'|"(?:\"|[^"])*"|[^#\n]+)?\s*(?:\s*\#.*)?\z`2223// Pattern for detecting valid variable within a value24variablePattern = `(\\)?(\$)(\{?([A-Z0-9_]+)?\}?)`25)2627// Byte order mark character28var (29bomUTF8 = []byte("\xEF\xBB\xBF")30bomUTF16LE = []byte("\xFF\xFE")31bomUTF16BE = []byte("\xFE\xFF")32)3334// Env holds key/value pair of valid environment variable35type Env map[string]string3637// 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.38// When it's called with no argument, it will load `.env` file on the current path and set the environment variables.39// Otherwise, it will loop over the filenames parameter and set the proper environment variables.40func Load(filenames ...string) error {41return loadenv(false, filenames...)42}4344// OverLoad is a function to load a file or multiple files and then export and override the valid variables into environment variables.45func OverLoad(filenames ...string) error {46return loadenv(true, filenames...)47}4849// Must is wrapper function that will panic when supplied function returns an error.50func Must(fn func(filenames ...string) error, filenames ...string) {51if err := fn(filenames...); err != nil {52panic(err.Error())53}54}5556// Apply is a function to load an io Reader then export the valid variables into environment variables if they do not exist.57func Apply(r io.Reader) error {58return parset(r, false)59}6061// OverApply is a function to load an io Reader then export and override the valid variables into environment variables.62func OverApply(r io.Reader) error {63return parset(r, true)64}6566func loadenv(override bool, filenames ...string) error {67if len(filenames) == 0 {68filenames = []string{".env"}69}7071for _, filename := range filenames {72f, err := os.Open(filename)73if err != nil {74return err75}7677err = parset(f, override)78f.Close()79if err != nil {80return err81}82}8384return nil85}8687// parse and set :)88func parset(r io.Reader, override bool) error {89env, err := strictParse(r, override)90if err != nil {91return err92}9394for key, val := range env {95setenv(key, val, override)96}9798return nil99}100101func setenv(key, val string, override bool) {102if override {103os.Setenv(key, val)104} else {105if _, present := os.LookupEnv(key); !present {106os.Setenv(key, val)107}108}109}110111// Parse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables.112// It expands the value of a variable from the environment variable but does not set the value to the environment itself.113// This function is skipping any invalid lines and only processing the valid one.114func Parse(r io.Reader) Env {115env, _ := strictParse(r, false)116return env117}118119// StrictParse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables.120// It expands the value of a variable from the environment variable but does not set the value to the environment itself.121// This function is returning an error if there are any invalid lines.122func StrictParse(r io.Reader) (Env, error) {123return strictParse(r, false)124}125126// Read is a function to parse a file line by line and returns the valid Env key/value pair of valid variables.127// It expands the value of a variable from the environment variable but does not set the value to the environment itself.128// This function is skipping any invalid lines and only processing the valid one.129func Read(filename string) (Env, error) {130f, err := os.Open(filename)131if err != nil {132return nil, err133}134defer f.Close()135return strictParse(f, false)136}137138// Unmarshal reads a string line by line and returns the valid Env key/value pair of valid variables.139// It expands the value of a variable from the environment variable but does not set the value to the environment itself.140// This function is returning an error if there are any invalid lines.141func Unmarshal(str string) (Env, error) {142return strictParse(strings.NewReader(str), false)143}144145// Marshal outputs the given environment as a env file.146// Variables will be sorted by name.147func Marshal(env Env) (string, error) {148lines := make([]string, 0, len(env))149for k, v := range env {150if d, err := strconv.Atoi(v); err == nil {151lines = append(lines, fmt.Sprintf(`%s=%d`, k, d))152} else {153lines = append(lines, fmt.Sprintf(`%s=%q`, k, v))154}155}156sort.Strings(lines)157return strings.Join(lines, "\n"), nil158}159160// Write serializes the given environment and writes it to a file161func Write(env Env, filename string) error {162content, err := Marshal(env)163if err != nil {164return err165}166// ensure the path exists167if err := os.MkdirAll(filepath.Dir(filename), 0o775); err != nil {168return err169}170// create or truncate the file171file, err := os.Create(filename)172if err != nil {173return err174}175defer file.Close()176_, err = file.WriteString(content + "\n")177if err != nil {178return err179}180181return file.Sync()182}183184// 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).185// If a CR is immediately followed by a LF, it is treated as a CRLF (one single line break).186func splitLines(data []byte, atEOF bool) (advance int, token []byte, err error) {187if atEOF && len(data) == 0 {188return 0, nil, bufio.ErrFinalToken189}190191idx := bytes.IndexAny(data, "\r\n")192switch {193case atEOF && idx < 0:194return len(data), data, bufio.ErrFinalToken195196case idx < 0:197return 0, nil, nil198}199200// consume CR or LF201eol := idx + 1202// detect CRLF203if len(data) > eol && data[eol-1] == '\r' && data[eol] == '\n' {204eol++205}206207return eol, data[:idx], nil208}209210func strictParse(r io.Reader, override bool) (Env, error) {211env := make(Env)212213buf := new(bytes.Buffer)214tee := io.TeeReader(r, buf)215216// There can be a maximum of 3 BOM bytes.217bomByteBuffer := make([]byte, 3)218_, err := tee.Read(bomByteBuffer)219if err != nil && err != io.EOF {220return env, err221}222223z := io.MultiReader(buf, r)224225// We chooes a different scanner depending on file encoding.226var scanner *bufio.Scanner227228if bytes.HasPrefix(bomByteBuffer, bomUTF8) {229scanner = bufio.NewScanner(transform.NewReader(z, unicode.UTF8BOM.NewDecoder()))230} else if bytes.HasPrefix(bomByteBuffer, bomUTF16LE) {231scanner = bufio.NewScanner(transform.NewReader(z, unicode.UTF16(unicode.LittleEndian, unicode.ExpectBOM).NewDecoder()))232} else if bytes.HasPrefix(bomByteBuffer, bomUTF16BE) {233scanner = bufio.NewScanner(transform.NewReader(z, unicode.UTF16(unicode.BigEndian, unicode.ExpectBOM).NewDecoder()))234} else {235scanner = bufio.NewScanner(z)236}237238scanner.Split(splitLines)239240for scanner.Scan() {241if err := scanner.Err(); err != nil {242return env, err243}244245line := strings.TrimSpace(scanner.Text())246if line == "" || line[0] == '#' {247continue248}249250quote := ""251// look for the delimiter character252idx := strings.Index(line, "=")253if idx == -1 {254idx = strings.Index(line, ":")255}256// look for a quote character257if idx > 0 && idx < len(line)-1 {258val := strings.TrimSpace(line[idx+1:])259if val[0] == '"' || val[0] == '\'' {260quote = val[:1]261// look for the closing quote character within the same line262idx = strings.LastIndex(strings.TrimSpace(val[1:]), quote)263if idx >= 0 && val[idx] != '\\' {264quote = ""265}266}267}268// look for the closing quote character269for quote != "" && scanner.Scan() {270l := scanner.Text()271line += "\n" + l272idx := strings.LastIndex(l, quote)273if idx > 0 && l[idx-1] == '\\' {274// foud a matching quote character but it's escaped275continue276}277if idx >= 0 {278// foud a matching quote279quote = ""280}281}282283if quote != "" {284return env, fmt.Errorf("missing quotes")285}286287err := parseLine(line, env, override)288if err != nil {289return env, err290}291}292293return env, scanner.Err()294}295296var (297lineRgx = regexp.MustCompile(linePattern)298unescapeRgx = regexp.MustCompile(`\\([^$])`)299varRgx = regexp.MustCompile(variablePattern)300)301302func parseLine(s string, env Env, override bool) error {303rm := lineRgx.FindStringSubmatch(s)304305if len(rm) == 0 {306return checkFormat(s, env)307}308309key := strings.TrimSpace(rm[1])310val := strings.TrimSpace(rm[2])311312var hsq, hdq bool313314// check if the value is quoted315if l := len(val); l >= 2 {316l -= 1317// has double quotes318hdq = val[0] == '"' && val[l] == '"'319// has single quotes320hsq = val[0] == '\'' && val[l] == '\''321322// remove quotes '' or ""323if hsq || hdq {324val = val[1:l]325}326}327328if hdq {329val = strings.ReplaceAll(val, `\n`, "\n")330val = strings.ReplaceAll(val, `\r`, "\r")331332// Unescape all characters except $ so variables can be escaped properly333val = unescapeRgx.ReplaceAllString(val, "$1")334}335336if !hsq {337fv := func(s string) string {338return varReplacement(s, hsq, env, override)339}340val = varRgx.ReplaceAllStringFunc(val, fv)341}342343env[key] = val344return nil345}346347func parseExport(st string, env Env) error {348if strings.HasPrefix(st, "export") {349vs := strings.SplitN(st, " ", 2)350351if len(vs) > 1 {352if _, ok := env[vs[1]]; !ok {353return fmt.Errorf("line `%s` has an unset variable", st)354}355}356}357358return nil359}360361var varNameRgx = regexp.MustCompile(`(\$)(\{?([A-Z0-9_]+)\}?)`)362363func varReplacement(s string, hsq bool, env Env, override bool) string {364if s == "" {365return s366}367368if s[0] == '\\' {369// the dollar sign is escaped370return s[1:]371}372373if hsq {374return s375}376377mn := varNameRgx.FindStringSubmatch(s)378379if len(mn) == 0 {380return s381}382383v := mn[3]384385if replace, ok := os.LookupEnv(v); ok && !override {386return replace387}388389if replace, ok := env[v]; ok {390return replace391}392393return os.Getenv(v)394}395396func checkFormat(s string, env Env) error {397st := strings.TrimSpace(s)398399if st == "" || st[0] == '#' {400return nil401}402403if err := parseExport(st, env); err != nil {404return err405}406407return fmt.Errorf("line `%s` doesn't match format", s)408}409410411