...

Source file src/github.com/joho/godotenv/godotenv.go

Documentation: github.com/joho/godotenv

     1  // Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv)
     2  //
     3  // Examples/readme can be found on the github page at https://github.com/joho/godotenv
     4  //
     5  // The TL;DR is that you make a .env file that looks something like
     6  //
     7  // 		SOME_ENV_VAR=somevalue
     8  //
     9  // and then in your go code you can call
    10  //
    11  // 		godotenv.Load()
    12  //
    13  // and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR")
    14  package godotenv
    15  
    16  import (
    17  	"bufio"
    18  	"errors"
    19  	"fmt"
    20  	"io"
    21  	"os"
    22  	"os/exec"
    23  	"regexp"
    24  	"sort"
    25  	"strings"
    26  )
    27  
    28  const doubleQuoteSpecialChars = "\\\n\r\"!$`"
    29  
    30  // Load will read your env file(s) and load them into ENV for this process.
    31  //
    32  // Call this function as close as possible to the start of your program (ideally in main)
    33  //
    34  // If you call Load without any args it will default to loading .env in the current path
    35  //
    36  // You can otherwise tell it which files to load (there can be more than one) like
    37  //
    38  //		godotenv.Load("fileone", "filetwo")
    39  //
    40  // It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults
    41  func Load(filenames ...string) (err error) {
    42  	filenames = filenamesOrDefault(filenames)
    43  
    44  	for _, filename := range filenames {
    45  		err = loadFile(filename, false)
    46  		if err != nil {
    47  			return // return early on a spazout
    48  		}
    49  	}
    50  	return
    51  }
    52  
    53  // Overload will read your env file(s) and load them into ENV for this process.
    54  //
    55  // Call this function as close as possible to the start of your program (ideally in main)
    56  //
    57  // If you call Overload without any args it will default to loading .env in the current path
    58  //
    59  // You can otherwise tell it which files to load (there can be more than one) like
    60  //
    61  //		godotenv.Overload("fileone", "filetwo")
    62  //
    63  // It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars.
    64  func Overload(filenames ...string) (err error) {
    65  	filenames = filenamesOrDefault(filenames)
    66  
    67  	for _, filename := range filenames {
    68  		err = loadFile(filename, true)
    69  		if err != nil {
    70  			return // return early on a spazout
    71  		}
    72  	}
    73  	return
    74  }
    75  
    76  // Read all env (with same file loading semantics as Load) but return values as
    77  // a map rather than automatically writing values into env
    78  func Read(filenames ...string) (envMap map[string]string, err error) {
    79  	filenames = filenamesOrDefault(filenames)
    80  	envMap = make(map[string]string)
    81  
    82  	for _, filename := range filenames {
    83  		individualEnvMap, individualErr := readFile(filename)
    84  
    85  		if individualErr != nil {
    86  			err = individualErr
    87  			return // return early on a spazout
    88  		}
    89  
    90  		for key, value := range individualEnvMap {
    91  			envMap[key] = value
    92  		}
    93  	}
    94  
    95  	return
    96  }
    97  
    98  // Parse reads an env file from io.Reader, returning a map of keys and values.
    99  func Parse(r io.Reader) (envMap map[string]string, err error) {
   100  	envMap = make(map[string]string)
   101  
   102  	var lines []string
   103  	scanner := bufio.NewScanner(r)
   104  	for scanner.Scan() {
   105  		lines = append(lines, scanner.Text())
   106  	}
   107  
   108  	if err = scanner.Err(); err != nil {
   109  		return
   110  	}
   111  
   112  	for _, fullLine := range lines {
   113  		if !isIgnoredLine(fullLine) {
   114  			var key, value string
   115  			key, value, err = parseLine(fullLine, envMap)
   116  
   117  			if err != nil {
   118  				return
   119  			}
   120  			envMap[key] = value
   121  		}
   122  	}
   123  	return
   124  }
   125  
   126  //Unmarshal reads an env file from a string, returning a map of keys and values.
   127  func Unmarshal(str string) (envMap map[string]string, err error) {
   128  	return Parse(strings.NewReader(str))
   129  }
   130  
   131  // Exec loads env vars from the specified filenames (empty map falls back to default)
   132  // then executes the cmd specified.
   133  //
   134  // Simply hooks up os.Stdin/err/out to the command and calls Run()
   135  //
   136  // If you want more fine grained control over your command it's recommended
   137  // that you use `Load()` or `Read()` and the `os/exec` package yourself.
   138  func Exec(filenames []string, cmd string, cmdArgs []string) error {
   139  	Load(filenames...)
   140  
   141  	command := exec.Command(cmd, cmdArgs...)
   142  	command.Stdin = os.Stdin
   143  	command.Stdout = os.Stdout
   144  	command.Stderr = os.Stderr
   145  	return command.Run()
   146  }
   147  
   148  // Write serializes the given environment and writes it to a file
   149  func Write(envMap map[string]string, filename string) error {
   150  	content, error := Marshal(envMap)
   151  	if error != nil {
   152  		return error
   153  	}
   154  	file, error := os.Create(filename)
   155  	if error != nil {
   156  		return error
   157  	}
   158  	_, err := file.WriteString(content)
   159  	return err
   160  }
   161  
   162  // Marshal outputs the given environment as a dotenv-formatted environment file.
   163  // Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped.
   164  func Marshal(envMap map[string]string) (string, error) {
   165  	lines := make([]string, 0, len(envMap))
   166  	for k, v := range envMap {
   167  		lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v)))
   168  	}
   169  	sort.Strings(lines)
   170  	return strings.Join(lines, "\n"), nil
   171  }
   172  
   173  func filenamesOrDefault(filenames []string) []string {
   174  	if len(filenames) == 0 {
   175  		return []string{".env"}
   176  	}
   177  	return filenames
   178  }
   179  
   180  func loadFile(filename string, overload bool) error {
   181  	envMap, err := readFile(filename)
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	currentEnv := map[string]bool{}
   187  	rawEnv := os.Environ()
   188  	for _, rawEnvLine := range rawEnv {
   189  		key := strings.Split(rawEnvLine, "=")[0]
   190  		currentEnv[key] = true
   191  	}
   192  
   193  	for key, value := range envMap {
   194  		if !currentEnv[key] || overload {
   195  			os.Setenv(key, value)
   196  		}
   197  	}
   198  
   199  	return nil
   200  }
   201  
   202  func readFile(filename string) (envMap map[string]string, err error) {
   203  	file, err := os.Open(filename)
   204  	if err != nil {
   205  		return
   206  	}
   207  	defer file.Close()
   208  
   209  	return Parse(file)
   210  }
   211  
   212  func parseLine(line string, envMap map[string]string) (key string, value string, err error) {
   213  	if len(line) == 0 {
   214  		err = errors.New("zero length string")
   215  		return
   216  	}
   217  
   218  	// ditch the comments (but keep quoted hashes)
   219  	if strings.Contains(line, "#") {
   220  		segmentsBetweenHashes := strings.Split(line, "#")
   221  		quotesAreOpen := false
   222  		var segmentsToKeep []string
   223  		for _, segment := range segmentsBetweenHashes {
   224  			if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 {
   225  				if quotesAreOpen {
   226  					quotesAreOpen = false
   227  					segmentsToKeep = append(segmentsToKeep, segment)
   228  				} else {
   229  					quotesAreOpen = true
   230  				}
   231  			}
   232  
   233  			if len(segmentsToKeep) == 0 || quotesAreOpen {
   234  				segmentsToKeep = append(segmentsToKeep, segment)
   235  			}
   236  		}
   237  
   238  		line = strings.Join(segmentsToKeep, "#")
   239  	}
   240  
   241  	firstEquals := strings.Index(line, "=")
   242  	firstColon := strings.Index(line, ":")
   243  	splitString := strings.SplitN(line, "=", 2)
   244  	if firstColon != -1 && (firstColon < firstEquals || firstEquals == -1) {
   245  		//this is a yaml-style line
   246  		splitString = strings.SplitN(line, ":", 2)
   247  	}
   248  
   249  	if len(splitString) != 2 {
   250  		err = errors.New("Can't separate key from value")
   251  		return
   252  	}
   253  
   254  	// Parse the key
   255  	key = splitString[0]
   256  	if strings.HasPrefix(key, "export") {
   257  		key = strings.TrimPrefix(key, "export")
   258  	}
   259  	key = strings.Trim(key, " ")
   260  
   261  	// Parse the value
   262  	value = parseValue(splitString[1], envMap)
   263  	return
   264  }
   265  
   266  func parseValue(value string, envMap map[string]string) string {
   267  
   268  	// trim
   269  	value = strings.Trim(value, " ")
   270  
   271  	// check if we've got quoted values or possible escapes
   272  	if len(value) > 1 {
   273  		rs := regexp.MustCompile(`\A'(.*)'\z`)
   274  		singleQuotes := rs.FindStringSubmatch(value)
   275  
   276  		rd := regexp.MustCompile(`\A"(.*)"\z`)
   277  		doubleQuotes := rd.FindStringSubmatch(value)
   278  
   279  		if singleQuotes != nil || doubleQuotes != nil {
   280  			// pull the quotes off the edges
   281  			value = value[1 : len(value)-1]
   282  		}
   283  
   284  		if doubleQuotes != nil {
   285  			// expand newlines
   286  			escapeRegex := regexp.MustCompile(`\\.`)
   287  			value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string {
   288  				c := strings.TrimPrefix(match, `\`)
   289  				switch c {
   290  				case "n":
   291  					return "\n"
   292  				case "r":
   293  					return "\r"
   294  				default:
   295  					return match
   296  				}
   297  			})
   298  			// unescape characters
   299  			e := regexp.MustCompile(`\\([^$])`)
   300  			value = e.ReplaceAllString(value, "$1")
   301  		}
   302  
   303  		if singleQuotes == nil {
   304  			value = expandVariables(value, envMap)
   305  		}
   306  	}
   307  
   308  	return value
   309  }
   310  
   311  func expandVariables(v string, m map[string]string) string {
   312  	r := regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)
   313  
   314  	return r.ReplaceAllStringFunc(v, func(s string) string {
   315  		submatch := r.FindStringSubmatch(s)
   316  
   317  		if submatch == nil {
   318  			return s
   319  		}
   320  		if submatch[1] == "\\" || submatch[2] == "(" {
   321  			return submatch[0][1:]
   322  		} else if submatch[4] != "" {
   323  			return m[submatch[4]]
   324  		}
   325  		return s
   326  	})
   327  }
   328  
   329  func isIgnoredLine(line string) bool {
   330  	trimmedLine := strings.Trim(line, " \n\t")
   331  	return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
   332  }
   333  
   334  func doubleQuoteEscape(line string) string {
   335  	for _, c := range doubleQuoteSpecialChars {
   336  		toReplace := "\\" + string(c)
   337  		if c == '\n' {
   338  			toReplace = `\n`
   339  		}
   340  		if c == '\r' {
   341  			toReplace = `\r`
   342  		}
   343  		line = strings.Replace(line, string(c), toReplace, -1)
   344  	}
   345  	return line
   346  }
   347  

View as plain text