pax_global_header00006660000000000000000000000064140201247430014507gustar00rootroot0000000000000052 comment=ddf83eb33bbb136f62617a409142b74b91dbcff3 godotenv-1.4.0/000077500000000000000000000000001402012474300133365ustar00rootroot00000000000000godotenv-1.4.0/.github/000077500000000000000000000000001402012474300146765ustar00rootroot00000000000000godotenv-1.4.0/.github/workflows/000077500000000000000000000000001402012474300167335ustar00rootroot00000000000000godotenv-1.4.0/.github/workflows/ci.yml000066400000000000000000000035261402012474300200570ustar00rootroot00000000000000name: CI on: [push] jobs: test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: go: [ '1.15', '1.14' ] os: [ ubuntu-latest, macOS-latest, windows-latest ] name: ${{ matrix.os }} Go ${{ matrix.go }} Tests steps: - uses: actions/checkout@v2 - name: Setup go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go }} - run: go test test-non-amd64: strategy: fail-fast: false matrix: arch: # For some reasons this is segfaulting on go env # - name: IBM Z and LinuxONE # architecture: "s390x" - name: POWER8 architecture: "ppc64le" runs-on: ubuntu-latest name: Test on ${{ matrix.arch.name }} steps: - uses: actions/checkout@v2 - uses: uraimo/run-on-arch-action@master with: arch: ${{ matrix.arch.architecture }} distro: ubuntu20.04 env: | # YAML pipe GOARCH: ${{ matrix.arch.architecture }} CGO_ENABLED: 0 run: | apt-get update apt-get install -q -y curl wget git latestGo=$(curl "https://golang.org/VERSION?m=text") wget "https://dl.google.com/go/${latestGo}.linux-${GOARCH}.tar.gz" rm -f $(which go) rm -rf /usr/local/go tar -C /usr/local -xzf "${latestGo}.linux-${GOARCH}.tar.gz" export PATH=/usr/local/go/bin:$PATH printf "Using go at: $(which go)\n" printf "Go version: $(go version)\n" printf "\n\nGo environment:\n\n" go env printf "\n\nSystem environment:\n\n" env go get -insecure -v -t -d ./... go test ./... cd ./cmd/godotenv go build -trimpath -ldflags="-w -s" -vgodotenv-1.4.0/.github/workflows/release.yml000066400000000000000000000015411402012474300210770ustar00rootroot00000000000000on: push: tags: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 name: Upload Release Assets jobs: build: name: Upload Release Assets runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Generate build files uses: thatisuday/go-cross-build@v1 with: platforms: 'linux/amd64, linux/ppc64le, darwin/amd64, windows/amd64' package: 'cmd/godotenv' name: 'godotenv' compress: 'true' dest: 'dist' - name: Publish Binaries uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} release_name: Release ${{ github.ref }} tag: ${{ github.ref }} file: dist/* file_glob: true overwrite: true godotenv-1.4.0/.gitignore000066400000000000000000000000121402012474300153170ustar00rootroot00000000000000.DS_Store godotenv-1.4.0/LICENCE000066400000000000000000000020551402012474300143250ustar00rootroot00000000000000Copyright (c) 2013 John Barton MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. godotenv-1.4.0/README.md000066400000000000000000000125041402012474300146170ustar00rootroot00000000000000# GoDotEnv ![CI](https://github.com/joho/godotenv/workflows/CI/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/joho/godotenv)](https://goreportcard.com/report/github.com/joho/godotenv) A Go (golang) port of the Ruby dotenv project (which loads env vars from a .env file) From the original Library: > Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables. > > But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped. It can be used as a library (for loading in env for your own daemons etc) or as a bin command. There is test coverage and CI for both linuxish and windows environments, but I make no guarantees about the bin version working on windows. ## Installation As a library ```shell go get github.com/joho/godotenv ``` or if you want to use it as a bin command ```shell go get github.com/joho/godotenv/cmd/godotenv ``` ## Usage Add your application configuration to your `.env` file in the root of your project: ```shell S3_BUCKET=YOURS3BUCKET SECRET_KEY=YOURSECRETKEYGOESHERE ``` Then in your Go app you can do something like ```go package main import ( "github.com/joho/godotenv" "log" "os" ) func main() { err := godotenv.Load() if err != nil { log.Fatal("Error loading .env file") } s3Bucket := os.Getenv("S3_BUCKET") secretKey := os.Getenv("SECRET_KEY") // now do something with s3 or whatever } ``` If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import ```go import _ "github.com/joho/godotenv/autoload" ``` While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit ```go _ = godotenv.Load("somerandomfile") _ = godotenv.Load("filenumberone.env", "filenumbertwo.env") ``` If you want to be really fancy with your env file you can do comments and exports (below is a valid env file) ```shell # I am a comment and that is OK SOME_VAR=someval FOO=BAR # comments at line end are OK too export BAR=BAZ ``` Or finally you can do YAML(ish) style ```yaml FOO: bar BAR: baz ``` as a final aside, if you don't want godotenv munging your env you can just get a map back instead ```go var myEnv map[string]string myEnv, err := godotenv.Read() s3Bucket := myEnv["S3_BUCKET"] ``` ... or from an `io.Reader` instead of a local file ```go reader := getRemoteFile() myEnv, err := godotenv.Parse(reader) ``` ... or from a `string` if you so desire ```go content := getRemoteFileContent() myEnv, err := godotenv.Unmarshal(content) ``` ### Precedence & Conventions Existing envs take precedence of envs that are loaded later. The [convention](https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use) for managing multiple environments (i.e. development, test, production) is to create an env named `{YOURAPP}_ENV` and load envs in this order: ```go env := os.Getenv("FOO_ENV") if "" == env { env = "development" } godotenv.Load(".env." + env + ".local") if "test" != env { godotenv.Load(".env.local") } godotenv.Load(".env." + env) godotenv.Load() // The Original .env ``` If you need to, you can also use `godotenv.Overload()` to defy this convention and overwrite existing envs instead of only supplanting them. Use with caution. ### Command Mode Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH` ``` godotenv -f /some/path/to/.env some_command with some args ``` If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD` ### Writing Env Files Godotenv can also write a map representing the environment to a correctly-formatted and escaped file ```go env, err := godotenv.Unmarshal("KEY=value") err := godotenv.Write(env, "./.env") ``` ... or to a string ```go env, err := godotenv.Unmarshal("KEY=value") content, err := godotenv.Marshal(env) ``` ## Contributing Contributions are most welcome! The parser itself is pretty stupidly naive and I wouldn't be surprised if it breaks with edge cases. *code changes without tests will not be accepted* 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Added some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request ## Releases Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`. Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1` ## CI Linux: [![Build Status](https://travis-ci.org/joho/godotenv.svg?branch=master)](https://travis-ci.org/joho/godotenv) Windows: [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4)](https://ci.appveyor.com/project/joho/godotenv) ## Who? The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library. godotenv-1.4.0/autoload/000077500000000000000000000000001402012474300151465ustar00rootroot00000000000000godotenv-1.4.0/autoload/autoload.go000066400000000000000000000003501402012474300173030ustar00rootroot00000000000000package autoload /* You can just read the .env file on import just by doing import _ "github.com/joho/godotenv/autoload" And bob's your mother's brother */ import "github.com/joho/godotenv" func init() { godotenv.Load() } godotenv-1.4.0/cmd/000077500000000000000000000000001402012474300141015ustar00rootroot00000000000000godotenv-1.4.0/cmd/godotenv/000077500000000000000000000000001402012474300157265ustar00rootroot00000000000000godotenv-1.4.0/cmd/godotenv/cmd.go000066400000000000000000000017611402012474300170250ustar00rootroot00000000000000package main import ( "flag" "fmt" "log" "strings" "github.com/joho/godotenv" ) func main() { var showHelp bool flag.BoolVar(&showHelp, "h", false, "show help") var rawEnvFilenames string flag.StringVar(&rawEnvFilenames, "f", "", "comma separated paths to .env files") flag.Parse() usage := ` Run a process with an env setup from a .env file godotenv [-f ENV_FILE_PATHS] COMMAND_ARGS ENV_FILE_PATHS: comma separated paths to .env files COMMAND_ARGS: command and args you want to run example godotenv -f /path/to/something/.env,/another/path/.env fortune ` // if no args or -h flag // print usage and return args := flag.Args() if showHelp || len(args) == 0 { fmt.Println(usage) return } // load env var envFilenames []string if rawEnvFilenames != "" { envFilenames = strings.Split(rawEnvFilenames, ",") } // take rest of args and "exec" them cmd := args[0] cmdArgs := args[1:] err := godotenv.Exec(envFilenames, cmd, cmdArgs) if err != nil { log.Fatal(err) } } godotenv-1.4.0/fixtures/000077500000000000000000000000001402012474300152075ustar00rootroot00000000000000godotenv-1.4.0/fixtures/equals.env000066400000000000000000000001061402012474300172100ustar00rootroot00000000000000export OPTION_A='postgres://localhost:5432/database?sslmode=disable' godotenv-1.4.0/fixtures/exported.env000066400000000000000000000000471402012474300175540ustar00rootroot00000000000000export OPTION_A=2 export OPTION_B='\n' godotenv-1.4.0/fixtures/invalid1.env000066400000000000000000000000251402012474300174250ustar00rootroot00000000000000INVALID LINE foo=bar godotenv-1.4.0/fixtures/plain.env000066400000000000000000000001201402012474300170150ustar00rootroot00000000000000OPTION_A=1 OPTION_B=2 OPTION_C= 3 OPTION_D =4 OPTION_E = 5 OPTION_F = OPTION_G=godotenv-1.4.0/fixtures/quoted.env000066400000000000000000000002001402012474300172120ustar00rootroot00000000000000OPTION_A='1' OPTION_B='2' OPTION_C='' OPTION_D='\n' OPTION_E="1" OPTION_F="2" OPTION_G="" OPTION_H="\n" OPTION_I = "echo 'asd'" godotenv-1.4.0/fixtures/substitutions.env000066400000000000000000000001621402012474300206570ustar00rootroot00000000000000OPTION_A=1 OPTION_B=${OPTION_A} OPTION_C=$OPTION_B OPTION_D=${OPTION_A}${OPTION_B} OPTION_E=${OPTION_NOT_DEFINED} godotenv-1.4.0/go.mod000066400000000000000000000000511402012474300144400ustar00rootroot00000000000000module github.com/joho/godotenv go 1.12 godotenv-1.4.0/godotenv.go000066400000000000000000000220551402012474300155160ustar00rootroot00000000000000// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv) // // Examples/readme can be found on the github page at https://github.com/joho/godotenv // // The TL;DR is that you make a .env file that looks something like // // SOME_ENV_VAR=somevalue // // and then in your go code you can call // // godotenv.Load() // // and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR") package godotenv import ( "bufio" "errors" "fmt" "io" "os" "os/exec" "regexp" "sort" "strconv" "strings" ) const doubleQuoteSpecialChars = "\\\n\r\"!$`" // Load will read your env file(s) and load them into ENV for this process. // // Call this function as close as possible to the start of your program (ideally in main) // // If you call Load without any args it will default to loading .env in the current path // // You can otherwise tell it which files to load (there can be more than one) like // // godotenv.Load("fileone", "filetwo") // // 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 func Load(filenames ...string) (err error) { filenames = filenamesOrDefault(filenames) for _, filename := range filenames { err = loadFile(filename, false) if err != nil { return // return early on a spazout } } return } // Overload will read your env file(s) and load them into ENV for this process. // // Call this function as close as possible to the start of your program (ideally in main) // // If you call Overload without any args it will default to loading .env in the current path // // You can otherwise tell it which files to load (there can be more than one) like // // godotenv.Overload("fileone", "filetwo") // // It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars. func Overload(filenames ...string) (err error) { filenames = filenamesOrDefault(filenames) for _, filename := range filenames { err = loadFile(filename, true) if err != nil { return // return early on a spazout } } return } // Read all env (with same file loading semantics as Load) but return values as // a map rather than automatically writing values into env func Read(filenames ...string) (envMap map[string]string, err error) { filenames = filenamesOrDefault(filenames) envMap = make(map[string]string) for _, filename := range filenames { individualEnvMap, individualErr := readFile(filename) if individualErr != nil { err = individualErr return // return early on a spazout } for key, value := range individualEnvMap { envMap[key] = value } } return } // Parse reads an env file from io.Reader, returning a map of keys and values. func Parse(r io.Reader) (envMap map[string]string, err error) { envMap = make(map[string]string) var lines []string scanner := bufio.NewScanner(r) for scanner.Scan() { lines = append(lines, scanner.Text()) } if err = scanner.Err(); err != nil { return } for _, fullLine := range lines { if !isIgnoredLine(fullLine) { var key, value string key, value, err = parseLine(fullLine, envMap) if err != nil { return } envMap[key] = value } } return } //Unmarshal reads an env file from a string, returning a map of keys and values. func Unmarshal(str string) (envMap map[string]string, err error) { return Parse(strings.NewReader(str)) } // Exec loads env vars from the specified filenames (empty map falls back to default) // then executes the cmd specified. // // Simply hooks up os.Stdin/err/out to the command and calls Run() // // If you want more fine grained control over your command it's recommended // that you use `Load()` or `Read()` and the `os/exec` package yourself. func Exec(filenames []string, cmd string, cmdArgs []string) error { Load(filenames...) command := exec.Command(cmd, cmdArgs...) command.Stdin = os.Stdin command.Stdout = os.Stdout command.Stderr = os.Stderr return command.Run() } // Write serializes the given environment and writes it to a file func Write(envMap map[string]string, filename string) error { content, err := Marshal(envMap) if err != nil { return err } file, err := os.Create(filename) if err != nil { return err } defer file.Close() _, err = file.WriteString(content + "\n") if err != nil { return err } file.Sync() return err } // Marshal outputs the given environment as a dotenv-formatted environment file. // Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. func Marshal(envMap map[string]string) (string, error) { lines := make([]string, 0, len(envMap)) for k, v := range envMap { if d, err := strconv.Atoi(v); err == nil { lines = append(lines, fmt.Sprintf(`%s=%d`, k, d)) } else { lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) } } sort.Strings(lines) return strings.Join(lines, "\n"), nil } func filenamesOrDefault(filenames []string) []string { if len(filenames) == 0 { return []string{".env"} } return filenames } func loadFile(filename string, overload bool) error { envMap, err := readFile(filename) if err != nil { return err } currentEnv := map[string]bool{} rawEnv := os.Environ() for _, rawEnvLine := range rawEnv { key := strings.Split(rawEnvLine, "=")[0] currentEnv[key] = true } for key, value := range envMap { if !currentEnv[key] || overload { os.Setenv(key, value) } } return nil } func readFile(filename string) (envMap map[string]string, err error) { file, err := os.Open(filename) if err != nil { return } defer file.Close() return Parse(file) } var exportRegex = regexp.MustCompile(`^\s*(?:export\s+)?(.*?)\s*$`) func parseLine(line string, envMap map[string]string) (key string, value string, err error) { if len(line) == 0 { err = errors.New("zero length string") return } // ditch the comments (but keep quoted hashes) if strings.Contains(line, "#") { segmentsBetweenHashes := strings.Split(line, "#") quotesAreOpen := false var segmentsToKeep []string for _, segment := range segmentsBetweenHashes { if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 { if quotesAreOpen { quotesAreOpen = false segmentsToKeep = append(segmentsToKeep, segment) } else { quotesAreOpen = true } } if len(segmentsToKeep) == 0 || quotesAreOpen { segmentsToKeep = append(segmentsToKeep, segment) } } line = strings.Join(segmentsToKeep, "#") } firstEquals := strings.Index(line, "=") firstColon := strings.Index(line, ":") splitString := strings.SplitN(line, "=", 2) if firstColon != -1 && (firstColon < firstEquals || firstEquals == -1) { //this is a yaml-style line splitString = strings.SplitN(line, ":", 2) } if len(splitString) != 2 { err = errors.New("Can't separate key from value") return } // Parse the key key = splitString[0] if strings.HasPrefix(key, "export") { key = strings.TrimPrefix(key, "export") } key = strings.TrimSpace(key) key = exportRegex.ReplaceAllString(splitString[0], "$1") // Parse the value value = parseValue(splitString[1], envMap) return } var ( singleQuotesRegex = regexp.MustCompile(`\A'(.*)'\z`) doubleQuotesRegex = regexp.MustCompile(`\A"(.*)"\z`) escapeRegex = regexp.MustCompile(`\\.`) unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) ) func parseValue(value string, envMap map[string]string) string { // trim value = strings.Trim(value, " ") // check if we've got quoted values or possible escapes if len(value) > 1 { singleQuotes := singleQuotesRegex.FindStringSubmatch(value) doubleQuotes := doubleQuotesRegex.FindStringSubmatch(value) if singleQuotes != nil || doubleQuotes != nil { // pull the quotes off the edges value = value[1 : len(value)-1] } if doubleQuotes != nil { // expand newlines value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string { c := strings.TrimPrefix(match, `\`) switch c { case "n": return "\n" case "r": return "\r" default: return match } }) // unescape characters value = unescapeCharsRegex.ReplaceAllString(value, "$1") } if singleQuotes == nil { value = expandVariables(value, envMap) } } return value } var expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) func expandVariables(v string, m map[string]string) string { return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string { submatch := expandVarRegex.FindStringSubmatch(s) if submatch == nil { return s } if submatch[1] == "\\" || submatch[2] == "(" { return submatch[0][1:] } else if submatch[4] != "" { return m[submatch[4]] } return s }) } func isIgnoredLine(line string) bool { trimmedLine := strings.TrimSpace(line) return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#") } func doubleQuoteEscape(line string) string { for _, c := range doubleQuoteSpecialChars { toReplace := "\\" + string(c) if c == '\n' { toReplace = `\n` } if c == '\r' { toReplace = `\r` } line = strings.Replace(line, string(c), toReplace, -1) } return line } godotenv-1.4.0/godotenv_test.go000066400000000000000000000317651402012474300165650ustar00rootroot00000000000000package godotenv import ( "bytes" "fmt" "os" "reflect" "strings" "testing" ) var noopPresets = make(map[string]string) func parseAndCompare(t *testing.T, rawEnvLine string, expectedKey string, expectedValue string) { key, value, _ := parseLine(rawEnvLine, noopPresets) if key != expectedKey || value != expectedValue { t.Errorf("Expected '%v' to parse as '%v' => '%v', got '%v' => '%v' instead", rawEnvLine, expectedKey, expectedValue, key, value) } } func loadEnvAndCompareValues(t *testing.T, loader func(files ...string) error, envFileName string, expectedValues map[string]string, presets map[string]string) { // first up, clear the env os.Clearenv() for k, v := range presets { os.Setenv(k, v) } err := loader(envFileName) if err != nil { t.Fatalf("Error loading %v", envFileName) } for k := range expectedValues { envValue := os.Getenv(k) v := expectedValues[k] if envValue != v { t.Errorf("Mismatch for key '%v': expected '%v' got '%v'", k, v, envValue) } } } func TestLoadWithNoArgsLoadsDotEnv(t *testing.T) { err := Load() pathError := err.(*os.PathError) if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" { t.Errorf("Didn't try and open .env by default") } } func TestOverloadWithNoArgsOverloadsDotEnv(t *testing.T) { err := Overload() pathError := err.(*os.PathError) if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" { t.Errorf("Didn't try and open .env by default") } } func TestLoadFileNotFound(t *testing.T) { err := Load("somefilethatwillneverexistever.env") if err == nil { t.Error("File wasn't found but Load didn't return an error") } } func TestOverloadFileNotFound(t *testing.T) { err := Overload("somefilethatwillneverexistever.env") if err == nil { t.Error("File wasn't found but Overload didn't return an error") } } func TestReadPlainEnv(t *testing.T) { envFileName := "fixtures/plain.env" expectedValues := map[string]string{ "OPTION_A": "1", "OPTION_B": "2", "OPTION_C": "3", "OPTION_D": "4", "OPTION_E": "5", "OPTION_F": "", "OPTION_G": "", } envMap, err := Read(envFileName) if err != nil { t.Error("Error reading file") } if len(envMap) != len(expectedValues) { t.Error("Didn't get the right size map back") } for key, value := range expectedValues { if envMap[key] != value { t.Error("Read got one of the keys wrong") } } } func TestParse(t *testing.T) { envMap, err := Parse(bytes.NewReader([]byte("ONE=1\nTWO='2'\nTHREE = \"3\""))) expectedValues := map[string]string{ "ONE": "1", "TWO": "2", "THREE": "3", } if err != nil { t.Fatalf("error parsing env: %v", err) } for key, value := range expectedValues { if envMap[key] != value { t.Errorf("expected %s to be %s, got %s", key, value, envMap[key]) } } } func TestLoadDoesNotOverride(t *testing.T) { envFileName := "fixtures/plain.env" // ensure NO overload presets := map[string]string{ "OPTION_A": "do_not_override", "OPTION_B": "", } expectedValues := map[string]string{ "OPTION_A": "do_not_override", "OPTION_B": "", } loadEnvAndCompareValues(t, Load, envFileName, expectedValues, presets) } func TestOveroadDoesOverride(t *testing.T) { envFileName := "fixtures/plain.env" // ensure NO overload presets := map[string]string{ "OPTION_A": "do_not_override", } expectedValues := map[string]string{ "OPTION_A": "1", } loadEnvAndCompareValues(t, Overload, envFileName, expectedValues, presets) } func TestLoadPlainEnv(t *testing.T) { envFileName := "fixtures/plain.env" expectedValues := map[string]string{ "OPTION_A": "1", "OPTION_B": "2", "OPTION_C": "3", "OPTION_D": "4", "OPTION_E": "5", } loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) } func TestLoadExportedEnv(t *testing.T) { envFileName := "fixtures/exported.env" expectedValues := map[string]string{ "OPTION_A": "2", "OPTION_B": "\\n", } loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) } func TestLoadEqualsEnv(t *testing.T) { envFileName := "fixtures/equals.env" expectedValues := map[string]string{ "OPTION_A": "postgres://localhost:5432/database?sslmode=disable", } loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) } func TestLoadQuotedEnv(t *testing.T) { envFileName := "fixtures/quoted.env" expectedValues := map[string]string{ "OPTION_A": "1", "OPTION_B": "2", "OPTION_C": "", "OPTION_D": "\\n", "OPTION_E": "1", "OPTION_F": "2", "OPTION_G": "", "OPTION_H": "\n", "OPTION_I": "echo 'asd'", } loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) } func TestSubstitutions(t *testing.T) { envFileName := "fixtures/substitutions.env" expectedValues := map[string]string{ "OPTION_A": "1", "OPTION_B": "1", "OPTION_C": "1", "OPTION_D": "11", "OPTION_E": "", } loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) } func TestExpanding(t *testing.T) { tests := []struct { name string input string expected map[string]string }{ { "expands variables found in values", "FOO=test\nBAR=$FOO", map[string]string{"FOO": "test", "BAR": "test"}, }, { "parses variables wrapped in brackets", "FOO=test\nBAR=${FOO}bar", map[string]string{"FOO": "test", "BAR": "testbar"}, }, { "expands undefined variables to an empty string", "BAR=$FOO", map[string]string{"BAR": ""}, }, { "expands variables in double quoted strings", "FOO=test\nBAR=\"quote $FOO\"", map[string]string{"FOO": "test", "BAR": "quote test"}, }, { "does not expand variables in single quoted strings", "BAR='quote $FOO'", map[string]string{"BAR": "quote $FOO"}, }, { "does not expand escaped variables", `FOO="foo\$BAR"`, map[string]string{"FOO": "foo$BAR"}, }, { "does not expand escaped variables", `FOO="foo\${BAR}"`, map[string]string{"FOO": "foo${BAR}"}, }, { "does not expand escaped variables", "FOO=test\nBAR=\"foo\\${FOO} ${FOO}\"", map[string]string{"FOO": "test", "BAR": "foo${FOO} test"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { env, err := Parse(strings.NewReader(tt.input)) if err != nil { t.Errorf("Error: %s", err.Error()) } for k, v := range tt.expected { if strings.Compare(env[k], v) != 0 { t.Errorf("Expected: %s, Actual: %s", v, env[k]) } } }) } } func TestActualEnvVarsAreLeftAlone(t *testing.T) { os.Clearenv() os.Setenv("OPTION_A", "actualenv") _ = Load("fixtures/plain.env") if os.Getenv("OPTION_A") != "actualenv" { t.Error("An ENV var set earlier was overwritten") } } func TestParsing(t *testing.T) { // unquoted values parseAndCompare(t, "FOO=bar", "FOO", "bar") // parses values with spaces around equal sign parseAndCompare(t, "FOO =bar", "FOO", "bar") parseAndCompare(t, "FOO= bar", "FOO", "bar") // parses double quoted values parseAndCompare(t, `FOO="bar"`, "FOO", "bar") // parses single quoted values parseAndCompare(t, "FOO='bar'", "FOO", "bar") // parses escaped double quotes parseAndCompare(t, `FOO="escaped\"bar"`, "FOO", `escaped"bar`) // parses single quotes inside double quotes parseAndCompare(t, `FOO="'d'"`, "FOO", `'d'`) // parses yaml style options parseAndCompare(t, "OPTION_A: 1", "OPTION_A", "1") //parses yaml values with equal signs parseAndCompare(t, "OPTION_A: Foo=bar", "OPTION_A", "Foo=bar") // parses non-yaml options with colons parseAndCompare(t, "OPTION_A=1:B", "OPTION_A", "1:B") // parses export keyword parseAndCompare(t, "export OPTION_A=2", "OPTION_A", "2") parseAndCompare(t, `export OPTION_B='\n'`, "OPTION_B", "\\n") parseAndCompare(t, "export exportFoo=2", "exportFoo", "2") parseAndCompare(t, "exportFOO=2", "exportFOO", "2") parseAndCompare(t, "export_FOO =2", "export_FOO", "2") parseAndCompare(t, "export.FOO= 2", "export.FOO", "2") parseAndCompare(t, "export\tOPTION_A=2", "OPTION_A", "2") parseAndCompare(t, " export OPTION_A=2", "OPTION_A", "2") parseAndCompare(t, "\texport OPTION_A=2", "OPTION_A", "2") // it 'expands newlines in quoted strings' do // expect(env('FOO="bar\nbaz"')).to eql('FOO' => "bar\nbaz") parseAndCompare(t, `FOO="bar\nbaz"`, "FOO", "bar\nbaz") // it 'parses varibales with "." in the name' do // expect(env('FOO.BAR=foobar')).to eql('FOO.BAR' => 'foobar') parseAndCompare(t, "FOO.BAR=foobar", "FOO.BAR", "foobar") // it 'parses varibales with several "=" in the value' do // expect(env('FOO=foobar=')).to eql('FOO' => 'foobar=') parseAndCompare(t, "FOO=foobar=", "FOO", "foobar=") // it 'strips unquoted values' do // expect(env('foo=bar ')).to eql('foo' => 'bar') # not 'bar ' parseAndCompare(t, "FOO=bar ", "FOO", "bar") // it 'ignores inline comments' do // expect(env("foo=bar # this is foo")).to eql('foo' => 'bar') parseAndCompare(t, "FOO=bar # this is foo", "FOO", "bar") // it 'allows # in quoted value' do // expect(env('foo="bar#baz" # comment')).to eql('foo' => 'bar#baz') parseAndCompare(t, `FOO="bar#baz" # comment`, "FOO", "bar#baz") parseAndCompare(t, "FOO='bar#baz' # comment", "FOO", "bar#baz") parseAndCompare(t, `FOO="bar#baz#bang" # comment`, "FOO", "bar#baz#bang") // it 'parses # in quoted values' do // expect(env('foo="ba#r"')).to eql('foo' => 'ba#r') // expect(env("foo='ba#r'")).to eql('foo' => 'ba#r') parseAndCompare(t, `FOO="ba#r"`, "FOO", "ba#r") parseAndCompare(t, "FOO='ba#r'", "FOO", "ba#r") //newlines and backslashes should be escaped parseAndCompare(t, `FOO="bar\n\ b\az"`, "FOO", "bar\n baz") parseAndCompare(t, `FOO="bar\\\n\ b\az"`, "FOO", "bar\\\n baz") parseAndCompare(t, `FOO="bar\\r\ b\az"`, "FOO", "bar\\r baz") parseAndCompare(t, `="value"`, "", "value") parseAndCompare(t, `KEY="`, "KEY", "\"") parseAndCompare(t, `KEY="value`, "KEY", "\"value") // leading whitespace should be ignored parseAndCompare(t, " KEY =value", "KEY", "value") parseAndCompare(t, " KEY=value", "KEY", "value") parseAndCompare(t, "\tKEY=value", "KEY", "value") // it 'throws an error if line format is incorrect' do // expect{env('lol$wut')}.to raise_error(Dotenv::FormatError) badlyFormattedLine := "lol$wut" _, _, err := parseLine(badlyFormattedLine, noopPresets) if err == nil { t.Errorf("Expected \"%v\" to return error, but it didn't", badlyFormattedLine) } } func TestLinesToIgnore(t *testing.T) { // it 'ignores empty lines' do // expect(env("\n \t \nfoo=bar\n \nfizz=buzz")).to eql('foo' => 'bar', 'fizz' => 'buzz') if !isIgnoredLine("\n") { t.Error("Line with nothing but line break wasn't ignored") } if !isIgnoredLine("\r\n") { t.Error("Line with nothing but windows-style line break wasn't ignored") } if !isIgnoredLine("\t\t ") { t.Error("Line full of whitespace wasn't ignored") } // it 'ignores comment lines' do // expect(env("\n\n\n # HERE GOES FOO \nfoo=bar")).to eql('foo' => 'bar') if !isIgnoredLine("# comment") { t.Error("Comment wasn't ignored") } if !isIgnoredLine("\t#comment") { t.Error("Indented comment wasn't ignored") } // make sure we're not getting false positives if isIgnoredLine(`export OPTION_B='\n'`) { t.Error("ignoring a perfectly valid line to parse") } } func TestErrorReadDirectory(t *testing.T) { envFileName := "fixtures/" envMap, err := Read(envFileName) if err == nil { t.Errorf("Expected error, got %v", envMap) } } func TestErrorParsing(t *testing.T) { envFileName := "fixtures/invalid1.env" envMap, err := Read(envFileName) if err == nil { t.Errorf("Expected error, got %v", envMap) } } func TestWrite(t *testing.T) { writeAndCompare := func(env string, expected string) { envMap, _ := Unmarshal(env) actual, _ := Marshal(envMap) if expected != actual { t.Errorf("Expected '%v' (%v) to write as '%v', got '%v' instead.", env, envMap, expected, actual) } } //just test some single lines to show the general idea //TestRoundtrip makes most of the good assertions //values are always double-quoted writeAndCompare(`key=value`, `key="value"`) //double-quotes are escaped writeAndCompare(`key=va"lu"e`, `key="va\"lu\"e"`) //but single quotes are left alone writeAndCompare(`key=va'lu'e`, `key="va'lu'e"`) // newlines, backslashes, and some other special chars are escaped writeAndCompare(`foo="\n\r\\r!"`, `foo="\n\r\\r\!"`) // lines should be sorted writeAndCompare("foo=bar\nbaz=buzz", "baz=\"buzz\"\nfoo=\"bar\"") // integers should not be quoted writeAndCompare(`key="10"`, `key=10`) } func TestRoundtrip(t *testing.T) { fixtures := []string{"equals.env", "exported.env", "plain.env", "quoted.env"} for _, fixture := range fixtures { fixtureFilename := fmt.Sprintf("fixtures/%s", fixture) env, err := readFile(fixtureFilename) if err != nil { t.Errorf("Expected '%s' to read without error (%v)", fixtureFilename, err) } rep, err := Marshal(env) if err != nil { t.Errorf("Expected '%s' to Marshal (%v)", fixtureFilename, err) } roundtripped, err := Unmarshal(rep) if err != nil { t.Errorf("Expected '%s' to Mashal and Unmarshal (%v)", fixtureFilename, err) } if !reflect.DeepEqual(env, roundtripped) { t.Errorf("Expected '%s' to roundtrip as '%v', got '%v' instead", fixtureFilename, env, roundtripped) } } } godotenv-1.4.0/renovate.json000066400000000000000000000000511402012474300160500ustar00rootroot00000000000000{ "extends": [ "config:base" ] }