pax_global_header00006660000000000000000000000064143700217720014515gustar00rootroot0000000000000052 comment=3fc4292b58a67b78e1dbb6e47b4879a6cc602ec4 godotenv-1.5.1/000077500000000000000000000000001437002177200133465ustar00rootroot00000000000000godotenv-1.5.1/.github/000077500000000000000000000000001437002177200147065ustar00rootroot00000000000000godotenv-1.5.1/.github/dependabot.yml000066400000000000000000000003121437002177200175320ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "gomod" directory: / schedule: interval: "daily" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily"godotenv-1.5.1/.github/workflows/000077500000000000000000000000001437002177200167435ustar00rootroot00000000000000godotenv-1.5.1/.github/workflows/ci.yml000066400000000000000000000007151437002177200200640ustar00rootroot00000000000000name: CI on: [push] jobs: test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: go: [ '1.20', '1.19', '1.18', '1.17', '1.16' ] os: [ ubuntu-latest, macOS-latest, windows-latest ] name: ${{ matrix.os }} Go ${{ matrix.go }} Tests steps: - uses: actions/checkout@v3 - name: Setup go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - run: go test godotenv-1.5.1/.github/workflows/codeql-analysis.yml000066400000000000000000000052431437002177200225620ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ "main" ] pull_request: # The branches below must be a subset of the branches above branches: [ "main" ] schedule: - cron: '31 4 * * 2' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'go' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 godotenv-1.5.1/.github/workflows/release.yml000066400000000000000000000015511437002177200211100ustar00rootroot00000000000000on: 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@v3 - name: Generate build files uses: thatisuday/go-cross-build@v1.0.2 with: platforms: 'linux/amd64, linux/ppc64le, darwin/amd64, darwin/arm64, 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.5.1/.gitignore000066400000000000000000000000121437002177200153270ustar00rootroot00000000000000.DS_Store godotenv-1.5.1/LICENCE000066400000000000000000000020551437002177200143350ustar00rootroot00000000000000Copyright (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.5.1/README.md000066400000000000000000000137561437002177200146410ustar00rootroot00000000000000# 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](https://github.com/bkeepers/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 go >= 1.17 ```shell go install github.com/joho/godotenv/cmd/godotenv@latest ``` go < 1.17 ```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 ( "log" "os" "github.com/joho/godotenv" ) 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` By default, it won't override existing environment variables; you can do that with the `-o` flag. ### 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 welcome, but with some caveats. This library has been declared feature complete (see [#182](https://github.com/joho/godotenv/issues/182) for background) and will not be accepting issues or pull requests adding new functionality or breaking the library API. Contributions would be gladly accepted that: * bring this library's parsing into closer compatibility with the mainline dotenv implementations, in particular [Ruby's dotenv](https://github.com/bkeepers/dotenv) and [Node.js' dotenv](https://github.com/motdotla/dotenv) * keep the library up to date with the go ecosystem (ie CI bumps, documentation changes, changes in the core libraries) * bug fixes for use cases that pertain to the library's purpose of easing development of codebases deployed into twelve factor environments *code changes without tests and references to peer dotenv implementations 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` ## 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.5.1/autoload/000077500000000000000000000000001437002177200151565ustar00rootroot00000000000000godotenv-1.5.1/autoload/autoload.go000066400000000000000000000003501437002177200173130ustar00rootroot00000000000000package 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.5.1/cmd/000077500000000000000000000000001437002177200141115ustar00rootroot00000000000000godotenv-1.5.1/cmd/godotenv/000077500000000000000000000000001437002177200157365ustar00rootroot00000000000000godotenv-1.5.1/cmd/godotenv/cmd.go000066400000000000000000000021341437002177200170300ustar00rootroot00000000000000package 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") var overload bool flag.BoolVar(&overload, "o", false, "override existing .env variables") flag.Parse() usage := ` Run a process with an env setup from a .env file godotenv [-o] [-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, overload) if err != nil { log.Fatal(err) } } godotenv-1.5.1/fixtures/000077500000000000000000000000001437002177200152175ustar00rootroot00000000000000godotenv-1.5.1/fixtures/comments.env000066400000000000000000000000741437002177200175570ustar00rootroot00000000000000# Full line comment foo=bar # baz bar=foo#baz baz="foo"#bar godotenv-1.5.1/fixtures/equals.env000066400000000000000000000001051437002177200172170ustar00rootroot00000000000000export OPTION_A='postgres://localhost:5432/database?sslmode=disable' godotenv-1.5.1/fixtures/exported.env000066400000000000000000000000471437002177200175640ustar00rootroot00000000000000export OPTION_A=2 export OPTION_B='\n' godotenv-1.5.1/fixtures/invalid1.env000066400000000000000000000000251437002177200174350ustar00rootroot00000000000000INVALID LINE foo=bar godotenv-1.5.1/fixtures/plain.env000066400000000000000000000001351437002177200170330ustar00rootroot00000000000000OPTION_A=1 OPTION_B=2 OPTION_C= 3 OPTION_D =4 OPTION_E = 5 OPTION_F = OPTION_G= OPTION_H=1 2godotenv-1.5.1/fixtures/quoted.env000066400000000000000000000004341437002177200172330ustar00rootroot00000000000000OPTION_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'" OPTION_J='line 1 line 2' OPTION_K='line one this is \'quoted\' one more line' OPTION_L="line 1 line 2" OPTION_M="line one this is \"quoted\" one more line" godotenv-1.5.1/fixtures/substitutions.env000066400000000000000000000001621437002177200206670ustar00rootroot00000000000000OPTION_A=1 OPTION_B=${OPTION_A} OPTION_C=$OPTION_B OPTION_D=${OPTION_A}${OPTION_B} OPTION_E=${OPTION_NOT_DEFINED} godotenv-1.5.1/go.mod000066400000000000000000000000511437002177200144500ustar00rootroot00000000000000module github.com/joho/godotenv go 1.12 godotenv-1.5.1/godotenv.go000066400000000000000000000136041437002177200155260ustar00rootroot00000000000000// 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 ( "bytes" "fmt" "io" "os" "os/exec" "sort" "strconv" "strings" ) const doubleQuoteSpecialChars = "\\\n\r\"!$`" // Parse reads an env file from io.Reader, returning a map of keys and values. func Parse(r io.Reader) (map[string]string, error) { var buf bytes.Buffer _, err := io.Copy(&buf, r) if err != nil { return nil, err } return UnmarshalBytes(buf.Bytes()) } // 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 forcefully 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 } // 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 UnmarshalBytes([]byte(str)) } // UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values. func UnmarshalBytes(src []byte) (map[string]string, error) { out := make(map[string]string) err := parseBytes(src, out) return out, err } // 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()`, `Overload()` or `Read()` and the `os/exec` package yourself. func Exec(filenames []string, cmd string, cmdArgs []string, overload bool) error { op := Load if overload { op = Overload } if err := op(filenames...); err != nil { return err } 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 } return file.Sync() } // 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) } 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.5.1/godotenv_test.go000066400000000000000000000363311437002177200165670ustar00rootroot00000000000000package 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) { result, err := Unmarshal(rawEnvLine) if err != nil { t.Errorf("Expected %q to parse as %q: %q, errored %q", rawEnvLine, expectedKey, expectedValue, err) return } if result[expectedKey] != expectedValue { t.Errorf("Expected '%v' to parse as '%v' => '%v', got %q instead", rawEnvLine, expectedKey, expectedValue, result) } } 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": "", "OPTION_H": "1 2", } 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 TestOverloadDoesOverride(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", "OPTION_H": "1 2", } 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'", "OPTION_J": "line 1\nline 2", "OPTION_K": "line one\nthis is \\'quoted\\'\none more line", "OPTION_L": "line 1\nline 2", "OPTION_M": "line one\nthis is \"quoted\"\none more line", } 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 TestVariableStringValueSeparator(t *testing.T) { input := "TEST_URLS=\"stratum+tcp://stratum.antpool.com:3333\nstratum+tcp://stratum.antpool.com:443\"" want := map[string]string{ "TEST_URLS": "stratum+tcp://stratum.antpool.com:3333\nstratum+tcp://stratum.antpool.com:443", } got, err := Parse(strings.NewReader(input)) if err != nil { t.Error(err) } if len(got) != len(want) { t.Fatalf( "unexpected value:\nwant:\n\t%#v\n\ngot:\n\t%#v", want, got) } for k, wantVal := range want { gotVal, ok := got[k] if !ok { t.Fatalf("key %q doesn't present in result", k) } if wantVal != gotVal { t.Fatalf( "mismatch in %q value:\nwant:\n\t%s\n\ngot:\n\t%s", k, wantVal, gotVal) } } } 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 variables 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 variables 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") // unquoted internal whitespace is preserved parseAndCompare(t, `KEY=value value`, "KEY", "value value") // 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") // unquoted whitespace around keys 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 := Unmarshal(badlyFormattedLine) if err == nil { t.Errorf("Expected \"%v\" to return error, but it didn't", badlyFormattedLine) } } func TestLinesToIgnore(t *testing.T) { cases := map[string]struct { input string want string }{ "Line with nothing but line break": { input: "\n", }, "Line with nothing but windows-style line break": { input: "\r\n", }, "Line full of whitespace": { input: "\t\t ", }, "Comment": { input: "# Comment", }, "Indented comment": { input: "\t # comment", }, "non-ignored value": { input: `export OPTION_B='\n'`, want: `export OPTION_B='\n'`, }, } for n, c := range cases { t.Run(n, func(t *testing.T) { got := string(getStatementStart([]byte(c.input))) if got != c.want { t.Errorf("Expected:\t %q\nGot:\t %q", c.want, got) } }) } } 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 TestComments(t *testing.T) { envFileName := "fixtures/comments.env" expectedValues := map[string]string{ "foo": "bar", "bar": "foo#baz", "baz": "foo", } loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) } 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) } } } func TestTrailingNewlines(t *testing.T) { cases := map[string]struct { input string key string value string }{ "Simple value without trailing newline": { input: "KEY=value", key: "KEY", value: "value", }, "Value with internal whitespace without trailing newline": { input: "KEY=value value", key: "KEY", value: "value value", }, "Value with internal whitespace with trailing newline": { input: "KEY=value value\n", key: "KEY", value: "value value", }, "YAML style - value with internal whitespace without trailing newline": { input: "KEY: value value", key: "KEY", value: "value value", }, "YAML style - value with internal whitespace with trailing newline": { input: "KEY: value value\n", key: "KEY", value: "value value", }, } for n, c := range cases { t.Run(n, func(t *testing.T) { result, err := Unmarshal(c.input) if err != nil { t.Errorf("Input: %q Unexpected error:\t%q", c.input, err) } if result[c.key] != c.value { t.Errorf("Input %q Expected:\t %q/%q\nGot:\t %q", c.input, c.key, c.value, result) } }) } } godotenv-1.5.1/parser.go000066400000000000000000000137761437002177200152070ustar00rootroot00000000000000package godotenv import ( "bytes" "errors" "fmt" "regexp" "strings" "unicode" ) const ( charComment = '#' prefixSingleQuote = '\'' prefixDoubleQuote = '"' exportPrefix = "export" ) func parseBytes(src []byte, out map[string]string) error { src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1) cutset := src for { cutset = getStatementStart(cutset) if cutset == nil { // reached end of file break } key, left, err := locateKeyName(cutset) if err != nil { return err } value, left, err := extractVarValue(left, out) if err != nil { return err } out[key] = value cutset = left } return nil } // getStatementPosition returns position of statement begin. // // It skips any comment line or non-whitespace character. func getStatementStart(src []byte) []byte { pos := indexOfNonSpaceChar(src) if pos == -1 { return nil } src = src[pos:] if src[0] != charComment { return src } // skip comment section pos = bytes.IndexFunc(src, isCharFunc('\n')) if pos == -1 { return nil } return getStatementStart(src[pos:]) } // locateKeyName locates and parses key name and returns rest of slice func locateKeyName(src []byte) (key string, cutset []byte, err error) { // trim "export" and space at beginning src = bytes.TrimLeftFunc(src, isSpace) if bytes.HasPrefix(src, []byte(exportPrefix)) { trimmed := bytes.TrimPrefix(src, []byte(exportPrefix)) if bytes.IndexFunc(trimmed, isSpace) == 0 { src = bytes.TrimLeftFunc(trimmed, isSpace) } } // locate key name end and validate it in single loop offset := 0 loop: for i, char := range src { rchar := rune(char) if isSpace(rchar) { continue } switch char { case '=', ':': // library also supports yaml-style value declaration key = string(src[0:i]) offset = i + 1 break loop case '_': default: // variable name should match [A-Za-z0-9_.] if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' { continue } return "", nil, fmt.Errorf( `unexpected character %q in variable name near %q`, string(char), string(src)) } } if len(src) == 0 { return "", nil, errors.New("zero length string") } // trim whitespace key = strings.TrimRightFunc(key, unicode.IsSpace) cutset = bytes.TrimLeftFunc(src[offset:], isSpace) return key, cutset, nil } // extractVarValue extracts variable value and returns rest of slice func extractVarValue(src []byte, vars map[string]string) (value string, rest []byte, err error) { quote, hasPrefix := hasQuotePrefix(src) if !hasPrefix { // unquoted value - read until end of line endOfLine := bytes.IndexFunc(src, isLineEnd) // Hit EOF without a trailing newline if endOfLine == -1 { endOfLine = len(src) if endOfLine == 0 { return "", nil, nil } } // Convert line to rune away to do accurate countback of runes line := []rune(string(src[0:endOfLine])) // Assume end of line is end of var endOfVar := len(line) if endOfVar == 0 { return "", src[endOfLine:], nil } // Work backwards to check if the line ends in whitespace then // a comment (ie asdasd # some comment) for i := endOfVar - 1; i >= 0; i-- { if line[i] == charComment && i > 0 { if isSpace(line[i-1]) { endOfVar = i break } } } trimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace) return expandVariables(trimmed, vars), src[endOfLine:], nil } // lookup quoted string terminator for i := 1; i < len(src); i++ { if char := src[i]; char != quote { continue } // skip escaped quote symbol (\" or \', depends on quote) if prevChar := src[i-1]; prevChar == '\\' { continue } // trim quotes trimFunc := isCharFunc(rune(quote)) value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc)) if quote == prefixDoubleQuote { // unescape newlines for double quote (this is compat feature) // and expand environment variables value = expandVariables(expandEscapes(value), vars) } return value, src[i+1:], nil } // return formatted error if quoted string is not terminated valEndIndex := bytes.IndexFunc(src, isCharFunc('\n')) if valEndIndex == -1 { valEndIndex = len(src) } return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex]) } func expandEscapes(str string) string { out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string { c := strings.TrimPrefix(match, `\`) switch c { case "n": return "\n" case "r": return "\r" default: return match } }) return unescapeCharsRegex.ReplaceAllString(out, "$1") } func indexOfNonSpaceChar(src []byte) int { return bytes.IndexFunc(src, func(r rune) bool { return !unicode.IsSpace(r) }) } // hasQuotePrefix reports whether charset starts with single or double quote and returns quote character func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) { if len(src) == 0 { return 0, false } switch prefix := src[0]; prefix { case prefixDoubleQuote, prefixSingleQuote: return prefix, true default: return 0, false } } func isCharFunc(char rune) func(rune) bool { return func(v rune) bool { return v == char } } // isSpace reports whether the rune is a space character but not line break character // // this differs from unicode.IsSpace, which also applies line break as space func isSpace(r rune) bool { switch r { case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0: return true } return false } func isLineEnd(r rune) bool { if r == '\n' || r == '\r' { return true } return false } var ( escapeRegex = regexp.MustCompile(`\\.`) expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) ) 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 }) }