[
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: /\n    schedule:\n      interval: \"daily\"\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\""
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non: [push]\n\njobs:\n  test:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        go: [ '1.20', '1.19', '1.18', '1.17', '1.16' ]\n        os: [ ubuntu-latest, macOS-latest, windows-latest ]\n    name: ${{ matrix.os }} Go ${{ matrix.go }} Tests\n    steps:\n      - uses: actions/checkout@v3\n      - name: Setup go\n        uses: actions/setup-go@v4\n        with:\n          go-version: ${{ matrix.go }}\n      - run: go test\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ \"main\" ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ \"main\" ]\n  schedule:\n    - cron: '31 4 * * 2'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'go' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]\n        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v3\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v2\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        \n        # 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\n        # queries: security-extended,security-and-quality\n\n        \n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v2\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun\n\n    #   If the Autobuild fails above, remove it and uncomment the following three lines. \n    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.\n\n    # - run: |\n    #   echo \"Run, Build Application using script\"\n    #   ./location_of_script_within_repo/buildscript.sh\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v2\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "on:\n  push:\n    tags:\n    - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10\n\nname: Upload Release Assets\n\njobs:\n  build:\n    name: Upload Release Assets\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v3\n      - name: Generate build files\n        uses: thatisuday/go-cross-build@v1.0.2\n        with:\n            platforms: 'linux/amd64, linux/ppc64le, darwin/amd64, darwin/arm64, windows/amd64'\n            package: 'cmd/godotenv'\n            name: 'godotenv'\n            compress: 'true'\n            dest: 'dist'\n      - name: Publish Binaries\n        uses: svenstaro/upload-release-action@v2\n        with:\n          repo_token: ${{ secrets.GITHUB_TOKEN }}\n          release_name: Release ${{ github.ref }}\n          tag: ${{ github.ref }}\n          file: dist/*\n          file_glob: true\n          overwrite: true\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n"
  },
  {
    "path": "LICENCE",
    "content": "Copyright (c) 2013 John Barton\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "# 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)\n\nA Go (golang) port of the Ruby [dotenv](https://github.com/bkeepers/dotenv) project (which loads env vars from a .env file).\n\nFrom the original Library:\n\n> 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.\n>\n> 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.\n\nIt can be used as a library (for loading in env for your own daemons etc.) or as a bin command.\n\nThere is test coverage and CI for both linuxish and Windows environments, but I make no guarantees about the bin version working on Windows.\n\n## Installation\n\nAs a library\n\n```shell\ngo get github.com/joho/godotenv\n```\n\nor if you want to use it as a bin command\n\ngo >= 1.17\n```shell\ngo install github.com/joho/godotenv/cmd/godotenv@latest\n```\n\ngo < 1.17\n```shell\ngo get github.com/joho/godotenv/cmd/godotenv\n```\n\n## Usage\n\nAdd your application configuration to your `.env` file in the root of your project:\n\n```shell\nS3_BUCKET=YOURS3BUCKET\nSECRET_KEY=YOURSECRETKEYGOESHERE\n```\n\nThen in your Go app you can do something like\n\n```go\npackage main\n\nimport (\n    \"log\"\n    \"os\"\n\n    \"github.com/joho/godotenv\"\n)\n\nfunc main() {\n  err := godotenv.Load()\n  if err != nil {\n    log.Fatal(\"Error loading .env file\")\n  }\n\n  s3Bucket := os.Getenv(\"S3_BUCKET\")\n  secretKey := os.Getenv(\"SECRET_KEY\")\n\n  // now do something with s3 or whatever\n}\n```\n\nIf you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import\n\n```go\nimport _ \"github.com/joho/godotenv/autoload\"\n```\n\nWhile `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit\n\n```go\ngodotenv.Load(\"somerandomfile\")\ngodotenv.Load(\"filenumberone.env\", \"filenumbertwo.env\")\n```\n\nIf you want to be really fancy with your env file you can do comments and exports (below is a valid env file)\n\n```shell\n# I am a comment and that is OK\nSOME_VAR=someval\nFOO=BAR # comments at line end are OK too\nexport BAR=BAZ\n```\n\nOr finally you can do YAML(ish) style\n\n```yaml\nFOO: bar\nBAR: baz\n```\n\nas a final aside, if you don't want godotenv munging your env you can just get a map back instead\n\n```go\nvar myEnv map[string]string\nmyEnv, err := godotenv.Read()\n\ns3Bucket := myEnv[\"S3_BUCKET\"]\n```\n\n... or from an `io.Reader` instead of a local file\n\n```go\nreader := getRemoteFile()\nmyEnv, err := godotenv.Parse(reader)\n```\n\n... or from a `string` if you so desire\n\n```go\ncontent := getRemoteFileContent()\nmyEnv, err := godotenv.Unmarshal(content)\n```\n\n### Precedence & Conventions\n\nExisting envs take precedence of envs that are loaded later.\n\nThe [convention](https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use)\nfor managing multiple environments (i.e. development, test, production)\nis to create an env named `{YOURAPP}_ENV` and load envs in this order:\n\n```go\nenv := os.Getenv(\"FOO_ENV\")\nif \"\" == env {\n  env = \"development\"\n}\n\ngodotenv.Load(\".env.\" + env + \".local\")\nif \"test\" != env {\n  godotenv.Load(\".env.local\")\n}\ngodotenv.Load(\".env.\" + env)\ngodotenv.Load() // The Original .env\n```\n\nIf you need to, you can also use `godotenv.Overload()` to defy this convention\nand overwrite existing envs instead of only supplanting them. Use with caution.\n\n### Command Mode\n\nAssuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH`\n\n```\ngodotenv -f /some/path/to/.env some_command with some args\n```\n\nIf you don't specify `-f` it will fall back on the default of loading `.env` in `PWD`\n\nBy default, it won't override existing environment variables; you can do that with the `-o` flag.\n\n### Writing Env Files\n\nGodotenv can also write a map representing the environment to a correctly-formatted and escaped file\n\n```go\nenv, err := godotenv.Unmarshal(\"KEY=value\")\nerr := godotenv.Write(env, \"./.env\")\n```\n\n... or to a string\n\n```go\nenv, err := godotenv.Unmarshal(\"KEY=value\")\ncontent, err := godotenv.Marshal(env)\n```\n\n## Contributing\n\nContributions are welcome, but with some caveats.\n\nThis 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.\n\nContributions would be gladly accepted that:\n\n* 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)\n* keep the library up to date with the go ecosystem (ie CI bumps, documentation changes, changes in the core libraries)\n* bug fixes for use cases that pertain to the library's purpose of easing development of codebases deployed into twelve factor environments\n\n*code changes without tests and references to peer dotenv implementations will not be accepted*\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Added some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create new Pull Request\n\n## Releases\n\nReleases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`.\n\nUse [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1`\n\n## Who?\n\nThe 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.\n"
  },
  {
    "path": "autoload/autoload.go",
    "content": "package autoload\n\n/*\n\tYou can just read the .env file on import just by doing\n\n\t\timport _ \"github.com/joho/godotenv/autoload\"\n\n\tAnd bob's your mother's brother\n*/\n\nimport \"github.com/joho/godotenv\"\n\nfunc init() {\n\tgodotenv.Load()\n}\n"
  },
  {
    "path": "cmd/godotenv/cmd.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\t\"github.com/joho/godotenv\"\n)\n\nfunc main() {\n\tvar showHelp bool\n\tflag.BoolVar(&showHelp, \"h\", false, \"show help\")\n\tvar rawEnvFilenames string\n\tflag.StringVar(&rawEnvFilenames, \"f\", \"\", \"comma separated paths to .env files\")\n\tvar overload bool\n\tflag.BoolVar(&overload, \"o\", false, \"override existing .env variables\")\n\n\tflag.Parse()\n\n\tusage := `\nRun a process with an env setup from a .env file\n\ngodotenv [-o] [-f ENV_FILE_PATHS] COMMAND_ARGS\n\nENV_FILE_PATHS: comma separated paths to .env files\nCOMMAND_ARGS: command and args you want to run\n\nexample\n  godotenv -f /path/to/something/.env,/another/path/.env fortune\n`\n\t// if no args or -h flag\n\t// print usage and return\n\targs := flag.Args()\n\tif showHelp || len(args) == 0 {\n\t\tfmt.Println(usage)\n\t\treturn\n\t}\n\n\t// load env\n\tvar envFilenames []string\n\tif rawEnvFilenames != \"\" {\n\t\tenvFilenames = strings.Split(rawEnvFilenames, \",\")\n\t}\n\n\t// take rest of args and \"exec\" them\n\tcmd := args[0]\n\tcmdArgs := args[1:]\n\n\terr := godotenv.Exec(envFilenames, cmd, cmdArgs, overload)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "fixtures/comments.env",
    "content": "# Full line comment\nqux=thud # fred # other\nthud=fred#qux # other\nfred=qux#baz # other # more\nfoo=bar # baz\nbar=foo#baz\nbaz=\"foo\"#bar\n"
  },
  {
    "path": "fixtures/equals.env",
    "content": "export OPTION_A='postgres://localhost:5432/database?sslmode=disable'\n"
  },
  {
    "path": "fixtures/exported.env",
    "content": "export OPTION_A=2\nexport OPTION_B='\\n'\n"
  },
  {
    "path": "fixtures/invalid1.env",
    "content": "INVALID LINE\nfoo=bar\n"
  },
  {
    "path": "fixtures/plain.env",
    "content": "OPTION_A=1\nOPTION_B=2\nOPTION_C= 3\nOPTION_D =4\nOPTION_E = 5\nOPTION_F = \nOPTION_G=\nOPTION_H=1 2"
  },
  {
    "path": "fixtures/quoted.env",
    "content": "OPTION_A='1'\nOPTION_B='2'\nOPTION_C=''\nOPTION_D='\\n'\nOPTION_E=\"1\"\nOPTION_F=\"2\"\nOPTION_G=\"\"\nOPTION_H=\"\\n\"\nOPTION_I = \"echo 'asd'\"\nOPTION_J='line 1\nline 2'\nOPTION_K='line one\nthis is \\'quoted\\'\none more line'\nOPTION_L=\"line 1\nline 2\"\nOPTION_M=\"line one\nthis is \\\"quoted\\\"\none more line\"\n"
  },
  {
    "path": "fixtures/substitutions.env",
    "content": "OPTION_A=1\nOPTION_B=${OPTION_A}\nOPTION_C=$OPTION_B\nOPTION_D=${OPTION_A}${OPTION_B}\nOPTION_E=${OPTION_NOT_DEFINED}\nOPTION_F=${GLOBAL_OPTION}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/joho/godotenv\n\ngo 1.12\n"
  },
  {
    "path": "godotenv.go",
    "content": "// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv)\n//\n// Examples/readme can be found on the GitHub page at https://github.com/joho/godotenv\n//\n// The TL;DR is that you make a .env file that looks something like\n//\n//\tSOME_ENV_VAR=somevalue\n//\n// and then in your go code you can call\n//\n//\tgodotenv.Load()\n//\n// and all the env vars declared in .env will be available through os.Getenv(\"SOME_ENV_VAR\")\npackage godotenv\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst doubleQuoteSpecialChars = \"\\\\\\n\\r\\\"!$`\"\n\n// Parse reads an env file from io.Reader, returning a map of keys and values.\nfunc Parse(r io.Reader) (map[string]string, error) {\n\tvar buf bytes.Buffer\n\t_, err := io.Copy(&buf, r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn UnmarshalBytes(buf.Bytes())\n}\n\n// Load will read your env file(s) and load them into ENV for this process.\n//\n// Call this function as close as possible to the start of your program (ideally in main).\n//\n// If you call Load without any args it will default to loading .env in the current path.\n//\n// You can otherwise tell it which files to load (there can be more than one) like:\n//\n//\tgodotenv.Load(\"fileone\", \"filetwo\")\n//\n// 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.\nfunc Load(filenames ...string) (err error) {\n\tfilenames = filenamesOrDefault(filenames)\n\n\tfor _, filename := range filenames {\n\t\terr = loadFile(filename, false)\n\t\tif err != nil {\n\t\t\treturn // return early on a spazout\n\t\t}\n\t}\n\treturn\n}\n\n// Overload will read your env file(s) and load them into ENV for this process.\n//\n// Call this function as close as possible to the start of your program (ideally in main).\n//\n// If you call Overload without any args it will default to loading .env in the current path.\n//\n// You can otherwise tell it which files to load (there can be more than one) like:\n//\n//\tgodotenv.Overload(\"fileone\", \"filetwo\")\n//\n// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefully set all vars.\nfunc Overload(filenames ...string) (err error) {\n\tfilenames = filenamesOrDefault(filenames)\n\n\tfor _, filename := range filenames {\n\t\terr = loadFile(filename, true)\n\t\tif err != nil {\n\t\t\treturn // return early on a spazout\n\t\t}\n\t}\n\treturn\n}\n\n// Read all env (with same file loading semantics as Load) but return values as\n// a map rather than automatically writing values into env\nfunc Read(filenames ...string) (envMap map[string]string, err error) {\n\tfilenames = filenamesOrDefault(filenames)\n\tenvMap = make(map[string]string)\n\n\tfor _, filename := range filenames {\n\t\tindividualEnvMap, individualErr := readFile(filename)\n\n\t\tif individualErr != nil {\n\t\t\terr = individualErr\n\t\t\treturn // return early on a spazout\n\t\t}\n\n\t\tfor key, value := range individualEnvMap {\n\t\t\tenvMap[key] = value\n\t\t}\n\t}\n\n\treturn\n}\n\n// Unmarshal reads an env file from a string, returning a map of keys and values.\nfunc Unmarshal(str string) (envMap map[string]string, err error) {\n\treturn UnmarshalBytes([]byte(str))\n}\n\n// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values.\nfunc UnmarshalBytes(src []byte) (map[string]string, error) {\n\tout := make(map[string]string)\n\terr := parseBytes(src, out)\n\n\treturn out, err\n}\n\n// Exec loads env vars from the specified filenames (empty map falls back to default)\n// then executes the cmd specified.\n//\n// Simply hooks up os.Stdin/err/out to the command and calls Run().\n//\n// If you want more fine grained control over your command it's recommended\n// that you use `Load()`, `Overload()` or `Read()` and the `os/exec` package yourself.\nfunc Exec(filenames []string, cmd string, cmdArgs []string, overload bool) error {\n\top := Load\n\tif overload {\n\t\top = Overload\n\t}\n\tif err := op(filenames...); err != nil {\n\t\treturn err\n\t}\n\n\tcommand := exec.Command(cmd, cmdArgs...)\n\tcommand.Stdin = os.Stdin\n\tcommand.Stdout = os.Stdout\n\tcommand.Stderr = os.Stderr\n\treturn command.Run()\n}\n\n// Write serializes the given environment and writes it to a file.\nfunc Write(envMap map[string]string, filename string) error {\n\tcontent, err := Marshal(envMap)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfile, err := os.Create(filename)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\t_, err = file.WriteString(content + \"\\n\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn file.Sync()\n}\n\n// Marshal outputs the given environment as a dotenv-formatted environment file.\n// Each line is in the format: KEY=\"VALUE\" where VALUE is backslash-escaped.\nfunc Marshal(envMap map[string]string) (string, error) {\n\tlines := make([]string, 0, len(envMap))\n\tfor k, v := range envMap {\n\t\tif d, err := strconv.Atoi(v); err == nil {\n\t\t\tlines = append(lines, fmt.Sprintf(`%s=%d`, k, d))\n\t\t} else {\n\t\t\tlines = append(lines, fmt.Sprintf(`%s=\"%s\"`, k, doubleQuoteEscape(v)))\n\t\t}\n\t}\n\tsort.Strings(lines)\n\treturn strings.Join(lines, \"\\n\"), nil\n}\n\nfunc filenamesOrDefault(filenames []string) []string {\n\tif len(filenames) == 0 {\n\t\treturn []string{\".env\"}\n\t}\n\treturn filenames\n}\n\nfunc loadFile(filename string, overload bool) error {\n\tenvMap, err := readFile(filename)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcurrentEnv := map[string]bool{}\n\trawEnv := os.Environ()\n\tfor _, rawEnvLine := range rawEnv {\n\t\tkey := strings.Split(rawEnvLine, \"=\")[0]\n\t\tcurrentEnv[key] = true\n\t}\n\n\tfor key, value := range envMap {\n\t\tif !currentEnv[key] || overload {\n\t\t\t_ = os.Setenv(key, value)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc readFile(filename string) (envMap map[string]string, err error) {\n\tfile, err := os.Open(filename)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer file.Close()\n\n\treturn Parse(file)\n}\n\nfunc doubleQuoteEscape(line string) string {\n\tfor _, c := range doubleQuoteSpecialChars {\n\t\ttoReplace := \"\\\\\" + string(c)\n\t\tif c == '\\n' {\n\t\t\ttoReplace = `\\n`\n\t\t}\n\t\tif c == '\\r' {\n\t\t\ttoReplace = `\\r`\n\t\t}\n\t\tline = strings.Replace(line, string(c), toReplace, -1)\n\t}\n\treturn line\n}\n"
  },
  {
    "path": "godotenv_test.go",
    "content": "package godotenv\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n)\n\nvar noopPresets = make(map[string]string)\n\nfunc parseAndCompare(t *testing.T, rawEnvLine string, expectedKey string, expectedValue string) {\n\tresult, err := Unmarshal(rawEnvLine)\n\n\tif err != nil {\n\t\tt.Errorf(\"Expected %q to parse as %q: %q, errored %q\", rawEnvLine, expectedKey, expectedValue, err)\n\t\treturn\n\t}\n\tif result[expectedKey] != expectedValue {\n\t\tt.Errorf(\"Expected '%v' to parse as '%v' => '%v', got %q instead\", rawEnvLine, expectedKey, expectedValue, result)\n\t}\n}\n\nfunc loadEnvAndCompareValues(t *testing.T, loader func(files ...string) error, envFileName string, expectedValues map[string]string, presets map[string]string) {\n\t// first up, clear the env\n\tos.Clearenv()\n\n\tfor k, v := range presets {\n\t\tos.Setenv(k, v)\n\t}\n\n\terr := loader(envFileName)\n\tif err != nil {\n\t\tt.Fatalf(\"Error loading %v\", envFileName)\n\t}\n\n\tfor k := range expectedValues {\n\t\tenvValue := os.Getenv(k)\n\t\tv := expectedValues[k]\n\t\tif envValue != v {\n\t\t\tt.Errorf(\"Mismatch for key '%v': expected '%#v' got '%#v'\", k, v, envValue)\n\t\t}\n\t}\n}\n\nfunc TestLoadWithNoArgsLoadsDotEnv(t *testing.T) {\n\terr := Load()\n\tpathError := err.(*os.PathError)\n\tif pathError == nil || pathError.Op != \"open\" || pathError.Path != \".env\" {\n\t\tt.Errorf(\"Didn't try and open .env by default\")\n\t}\n}\n\nfunc TestOverloadWithNoArgsOverloadsDotEnv(t *testing.T) {\n\terr := Overload()\n\tpathError := err.(*os.PathError)\n\tif pathError == nil || pathError.Op != \"open\" || pathError.Path != \".env\" {\n\t\tt.Errorf(\"Didn't try and open .env by default\")\n\t}\n}\n\nfunc TestLoadFileNotFound(t *testing.T) {\n\terr := Load(\"somefilethatwillneverexistever.env\")\n\tif err == nil {\n\t\tt.Error(\"File wasn't found but Load didn't return an error\")\n\t}\n}\n\nfunc TestOverloadFileNotFound(t *testing.T) {\n\terr := Overload(\"somefilethatwillneverexistever.env\")\n\tif err == nil {\n\t\tt.Error(\"File wasn't found but Overload didn't return an error\")\n\t}\n}\n\nfunc TestReadPlainEnv(t *testing.T) {\n\tenvFileName := \"fixtures/plain.env\"\n\texpectedValues := map[string]string{\n\t\t\"OPTION_A\": \"1\",\n\t\t\"OPTION_B\": \"2\",\n\t\t\"OPTION_C\": \"3\",\n\t\t\"OPTION_D\": \"4\",\n\t\t\"OPTION_E\": \"5\",\n\t\t\"OPTION_F\": \"\",\n\t\t\"OPTION_G\": \"\",\n\t\t\"OPTION_H\": \"1 2\",\n\t}\n\n\tenvMap, err := Read(envFileName)\n\tif err != nil {\n\t\tt.Error(\"Error reading file\")\n\t}\n\n\tif len(envMap) != len(expectedValues) {\n\t\tt.Error(\"Didn't get the right size map back\")\n\t}\n\n\tfor key, value := range expectedValues {\n\t\tif envMap[key] != value {\n\t\t\tt.Error(\"Read got one of the keys wrong\")\n\t\t}\n\t}\n}\n\nfunc TestParse(t *testing.T) {\n\tenvMap, err := Parse(bytes.NewReader([]byte(\"ONE=1\\nTWO='2'\\nTHREE = \\\"3\\\"\")))\n\texpectedValues := map[string]string{\n\t\t\"ONE\":   \"1\",\n\t\t\"TWO\":   \"2\",\n\t\t\"THREE\": \"3\",\n\t}\n\tif err != nil {\n\t\tt.Fatalf(\"error parsing env: %v\", err)\n\t}\n\tfor key, value := range expectedValues {\n\t\tif envMap[key] != value {\n\t\t\tt.Errorf(\"expected %s to be %s, got %s\", key, value, envMap[key])\n\t\t}\n\t}\n}\n\nfunc TestLoadDoesNotOverride(t *testing.T) {\n\tenvFileName := \"fixtures/plain.env\"\n\n\t// ensure NO overload\n\tpresets := map[string]string{\n\t\t\"OPTION_A\": \"do_not_override\",\n\t\t\"OPTION_B\": \"\",\n\t}\n\n\texpectedValues := map[string]string{\n\t\t\"OPTION_A\": \"do_not_override\",\n\t\t\"OPTION_B\": \"\",\n\t}\n\tloadEnvAndCompareValues(t, Load, envFileName, expectedValues, presets)\n}\n\nfunc TestOverloadDoesOverride(t *testing.T) {\n\tenvFileName := \"fixtures/plain.env\"\n\n\t// ensure NO overload\n\tpresets := map[string]string{\n\t\t\"OPTION_A\": \"do_not_override\",\n\t}\n\n\texpectedValues := map[string]string{\n\t\t\"OPTION_A\": \"1\",\n\t}\n\tloadEnvAndCompareValues(t, Overload, envFileName, expectedValues, presets)\n}\n\nfunc TestLoadPlainEnv(t *testing.T) {\n\tenvFileName := \"fixtures/plain.env\"\n\texpectedValues := map[string]string{\n\t\t\"OPTION_A\": \"1\",\n\t\t\"OPTION_B\": \"2\",\n\t\t\"OPTION_C\": \"3\",\n\t\t\"OPTION_D\": \"4\",\n\t\t\"OPTION_E\": \"5\",\n\t\t\"OPTION_H\": \"1 2\",\n\t}\n\n\tloadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)\n}\n\nfunc TestLoadExportedEnv(t *testing.T) {\n\tenvFileName := \"fixtures/exported.env\"\n\texpectedValues := map[string]string{\n\t\t\"OPTION_A\": \"2\",\n\t\t\"OPTION_B\": \"\\\\n\",\n\t}\n\n\tloadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)\n}\n\nfunc TestLoadEqualsEnv(t *testing.T) {\n\tenvFileName := \"fixtures/equals.env\"\n\texpectedValues := map[string]string{\n\t\t\"OPTION_A\": \"postgres://localhost:5432/database?sslmode=disable\",\n\t}\n\n\tloadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)\n}\n\nfunc TestLoadQuotedEnv(t *testing.T) {\n\tenvFileName := \"fixtures/quoted.env\"\n\texpectedValues := map[string]string{\n\t\t\"OPTION_A\": \"1\",\n\t\t\"OPTION_B\": \"2\",\n\t\t\"OPTION_C\": \"\",\n\t\t\"OPTION_D\": \"\\\\n\",\n\t\t\"OPTION_E\": \"1\",\n\t\t\"OPTION_F\": \"2\",\n\t\t\"OPTION_G\": \"\",\n\t\t\"OPTION_H\": \"\\n\",\n\t\t\"OPTION_I\": \"echo 'asd'\",\n\t\t\"OPTION_J\": \"line 1\\nline 2\",\n\t\t\"OPTION_K\": \"line one\\nthis is \\\\'quoted\\\\'\\none more line\",\n\t\t\"OPTION_L\": \"line 1\\nline 2\",\n\t\t\"OPTION_M\": \"line one\\nthis is \\\"quoted\\\"\\none more line\",\n\t}\n\n\tloadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)\n}\n\nfunc TestSubstitutions(t *testing.T) {\n\tenvFileName := \"fixtures/substitutions.env\"\n\n\tpresets := map[string]string{\n\t\t\"GLOBAL_OPTION\": \"global\",\n\t}\n\n\texpectedValues := map[string]string{\n\t\t\"OPTION_A\": \"1\",\n\t\t\"OPTION_B\": \"1\",\n\t\t\"OPTION_C\": \"1\",\n\t\t\"OPTION_D\": \"11\",\n\t\t\"OPTION_E\": \"\",\n\t\t\"OPTION_F\": \"global\",\n\t}\n\n\tloadEnvAndCompareValues(t, Load, envFileName, expectedValues, presets)\n}\n\nfunc TestExpanding(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected map[string]string\n\t}{\n\t\t{\n\t\t\t\"expands variables found in values\",\n\t\t\t\"FOO=test\\nBAR=$FOO\",\n\t\t\tmap[string]string{\"FOO\": \"test\", \"BAR\": \"test\"},\n\t\t},\n\t\t{\n\t\t\t\"parses variables wrapped in brackets\",\n\t\t\t\"FOO=test\\nBAR=${FOO}bar\",\n\t\t\tmap[string]string{\"FOO\": \"test\", \"BAR\": \"testbar\"},\n\t\t},\n\t\t{\n\t\t\t\"expands undefined variables to an empty string\",\n\t\t\t\"BAR=$FOO\",\n\t\t\tmap[string]string{\"BAR\": \"\"},\n\t\t},\n\t\t{\n\t\t\t\"expands variables in double quoted strings\",\n\t\t\t\"FOO=test\\nBAR=\\\"quote $FOO\\\"\",\n\t\t\tmap[string]string{\"FOO\": \"test\", \"BAR\": \"quote test\"},\n\t\t},\n\t\t{\n\t\t\t\"does not expand variables in single quoted strings\",\n\t\t\t\"BAR='quote $FOO'\",\n\t\t\tmap[string]string{\"BAR\": \"quote $FOO\"},\n\t\t},\n\t\t{\n\t\t\t\"does not expand escaped variables\",\n\t\t\t`FOO=\"foo\\$BAR\"`,\n\t\t\tmap[string]string{\"FOO\": \"foo$BAR\"},\n\t\t},\n\t\t{\n\t\t\t\"does not expand escaped variables\",\n\t\t\t`FOO=\"foo\\${BAR}\"`,\n\t\t\tmap[string]string{\"FOO\": \"foo${BAR}\"},\n\t\t},\n\t\t{\n\t\t\t\"does not expand escaped variables\",\n\t\t\t\"FOO=test\\nBAR=\\\"foo\\\\${FOO} ${FOO}\\\"\",\n\t\t\tmap[string]string{\"FOO\": \"test\", \"BAR\": \"foo${FOO} test\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tenv, err := Parse(strings.NewReader(tt.input))\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error: %s\", err.Error())\n\t\t\t}\n\t\t\tfor k, v := range tt.expected {\n\t\t\t\tif strings.Compare(env[k], v) != 0 {\n\t\t\t\t\tt.Errorf(\"Expected: %s, Actual: %s\", v, env[k])\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVariableStringValueSeparator(t *testing.T) {\n\tinput := \"TEST_URLS=\\\"stratum+tcp://stratum.antpool.com:3333\\nstratum+tcp://stratum.antpool.com:443\\\"\"\n\twant := map[string]string{\n\t\t\"TEST_URLS\": \"stratum+tcp://stratum.antpool.com:3333\\nstratum+tcp://stratum.antpool.com:443\",\n\t}\n\tgot, err := Parse(strings.NewReader(input))\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif len(got) != len(want) {\n\t\tt.Fatalf(\n\t\t\t\"unexpected value:\\nwant:\\n\\t%#v\\n\\ngot:\\n\\t%#v\", want, got)\n\t}\n\n\tfor k, wantVal := range want {\n\t\tgotVal, ok := got[k]\n\t\tif !ok {\n\t\t\tt.Fatalf(\"key %q doesn't present in result\", k)\n\t\t}\n\t\tif wantVal != gotVal {\n\t\t\tt.Fatalf(\n\t\t\t\t\"mismatch in %q value:\\nwant:\\n\\t%s\\n\\ngot:\\n\\t%s\", k,\n\t\t\t\twantVal, gotVal)\n\t\t}\n\t}\n}\n\nfunc TestActualEnvVarsAreLeftAlone(t *testing.T) {\n\tos.Clearenv()\n\tos.Setenv(\"OPTION_A\", \"actualenv\")\n\t_ = Load(\"fixtures/plain.env\")\n\n\tif os.Getenv(\"OPTION_A\") != \"actualenv\" {\n\t\tt.Error(\"An ENV var set earlier was overwritten\")\n\t}\n}\n\nfunc TestParsing(t *testing.T) {\n\t// unquoted values\n\tparseAndCompare(t, \"FOO=bar\", \"FOO\", \"bar\")\n\n\t// parses values with spaces around equal sign\n\tparseAndCompare(t, \"FOO =bar\", \"FOO\", \"bar\")\n\tparseAndCompare(t, \"FOO= bar\", \"FOO\", \"bar\")\n\n\t// parses double quoted values\n\tparseAndCompare(t, `FOO=\"bar\"`, \"FOO\", \"bar\")\n\n\t// parses single quoted values\n\tparseAndCompare(t, \"FOO='bar'\", \"FOO\", \"bar\")\n\n\t// parses escaped double quotes\n\tparseAndCompare(t, `FOO=\"escaped\\\"bar\"`, \"FOO\", `escaped\"bar`)\n\n\t// parses single quotes inside double quotes\n\tparseAndCompare(t, `FOO=\"'d'\"`, \"FOO\", `'d'`)\n\n\t// parses yaml style options\n\tparseAndCompare(t, \"OPTION_A: 1\", \"OPTION_A\", \"1\")\n\n\t//parses yaml values with equal signs\n\tparseAndCompare(t, \"OPTION_A: Foo=bar\", \"OPTION_A\", \"Foo=bar\")\n\n\t// parses non-yaml options with colons\n\tparseAndCompare(t, \"OPTION_A=1:B\", \"OPTION_A\", \"1:B\")\n\n\t// parses export keyword\n\tparseAndCompare(t, \"export OPTION_A=2\", \"OPTION_A\", \"2\")\n\tparseAndCompare(t, `export OPTION_B='\\n'`, \"OPTION_B\", \"\\\\n\")\n\tparseAndCompare(t, \"export exportFoo=2\", \"exportFoo\", \"2\")\n\tparseAndCompare(t, \"exportFOO=2\", \"exportFOO\", \"2\")\n\tparseAndCompare(t, \"export_FOO =2\", \"export_FOO\", \"2\")\n\tparseAndCompare(t, \"export.FOO= 2\", \"export.FOO\", \"2\")\n\tparseAndCompare(t, \"export\\tOPTION_A=2\", \"OPTION_A\", \"2\")\n\tparseAndCompare(t, \"  export OPTION_A=2\", \"OPTION_A\", \"2\")\n\tparseAndCompare(t, \"\\texport OPTION_A=2\", \"OPTION_A\", \"2\")\n\n\t// it 'expands newlines in quoted strings' do\n\t// expect(env('FOO=\"bar\\nbaz\"')).to eql('FOO' => \"bar\\nbaz\")\n\tparseAndCompare(t, `FOO=\"bar\\nbaz\"`, \"FOO\", \"bar\\nbaz\")\n\n\t// it 'parses variables with \".\" in the name' do\n\t// expect(env('FOO.BAR=foobar')).to eql('FOO.BAR' => 'foobar')\n\tparseAndCompare(t, \"FOO.BAR=foobar\", \"FOO.BAR\", \"foobar\")\n\n\t// it 'parses variables with several \"=\" in the value' do\n\t// expect(env('FOO=foobar=')).to eql('FOO' => 'foobar=')\n\tparseAndCompare(t, \"FOO=foobar=\", \"FOO\", \"foobar=\")\n\n\t// it 'strips unquoted values' do\n\t// expect(env('foo=bar ')).to eql('foo' => 'bar') # not 'bar '\n\tparseAndCompare(t, \"FOO=bar \", \"FOO\", \"bar\")\n\n\t// unquoted internal whitespace is preserved\n\tparseAndCompare(t, `KEY=value value`, \"KEY\", \"value value\")\n\n\t// it 'ignores inline comments' do\n\t// expect(env(\"foo=bar # this is foo\")).to eql('foo' => 'bar')\n\tparseAndCompare(t, \"FOO=bar # this is foo\", \"FOO\", \"bar\")\n\n\t// it 'allows # in quoted value' do\n\t// expect(env('foo=\"bar#baz\" # comment')).to eql('foo' => 'bar#baz')\n\tparseAndCompare(t, `FOO=\"bar#baz\" # comment`, \"FOO\", \"bar#baz\")\n\tparseAndCompare(t, \"FOO='bar#baz' # comment\", \"FOO\", \"bar#baz\")\n\tparseAndCompare(t, `FOO=\"bar#baz#bang\" # comment`, \"FOO\", \"bar#baz#bang\")\n\n\t// it 'parses # in quoted values' do\n\t// expect(env('foo=\"ba#r\"')).to eql('foo' => 'ba#r')\n\t// expect(env(\"foo='ba#r'\")).to eql('foo' => 'ba#r')\n\tparseAndCompare(t, `FOO=\"ba#r\"`, \"FOO\", \"ba#r\")\n\tparseAndCompare(t, \"FOO='ba#r'\", \"FOO\", \"ba#r\")\n\n\t//newlines and backslashes should be escaped\n\tparseAndCompare(t, `FOO=\"bar\\n\\ b\\az\"`, \"FOO\", \"bar\\n baz\")\n\tparseAndCompare(t, `FOO=\"bar\\\\\\n\\ b\\az\"`, \"FOO\", \"bar\\\\\\n baz\")\n\tparseAndCompare(t, `FOO=\"bar\\\\r\\ b\\az\"`, \"FOO\", \"bar\\\\r baz\")\n\n\tparseAndCompare(t, `=\"value\"`, \"\", \"value\")\n\n\t// unquoted whitespace around keys should be ignored\n\tparseAndCompare(t, \" KEY =value\", \"KEY\", \"value\")\n\tparseAndCompare(t, \"   KEY=value\", \"KEY\", \"value\")\n\tparseAndCompare(t, \"\\tKEY=value\", \"KEY\", \"value\")\n\n\t// it 'throws an error if line format is incorrect' do\n\t// expect{env('lol$wut')}.to raise_error(Dotenv::FormatError)\n\tbadlyFormattedLine := \"lol$wut\"\n\t_, err := Unmarshal(badlyFormattedLine)\n\tif err == nil {\n\t\tt.Errorf(\"Expected \\\"%v\\\" to return error, but it didn't\", badlyFormattedLine)\n\t}\n}\n\nfunc TestLinesToIgnore(t *testing.T) {\n\tcases := map[string]struct {\n\t\tinput string\n\t\twant  string\n\t}{\n\t\t\"Line with nothing but line break\": {\n\t\t\tinput: \"\\n\",\n\t\t},\n\t\t\"Line with nothing but windows-style line break\": {\n\t\t\tinput: \"\\r\\n\",\n\t\t},\n\t\t\"Line full of whitespace\": {\n\t\t\tinput: \"\\t\\t \",\n\t\t},\n\t\t\"Comment\": {\n\t\t\tinput: \"# Comment\",\n\t\t},\n\t\t\"Indented comment\": {\n\t\t\tinput: \"\\t # comment\",\n\t\t},\n\t\t\"non-ignored value\": {\n\t\t\tinput: `export OPTION_B='\\n'`,\n\t\t\twant:  `export OPTION_B='\\n'`,\n\t\t},\n\t}\n\n\tfor n, c := range cases {\n\t\tt.Run(n, func(t *testing.T) {\n\t\t\tgot := string(getStatementStart([]byte(c.input)))\n\t\t\tif got != c.want {\n\t\t\t\tt.Errorf(\"Expected:\\t %q\\nGot:\\t %q\", c.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestErrorReadDirectory(t *testing.T) {\n\tenvFileName := \"fixtures/\"\n\tenvMap, err := Read(envFileName)\n\n\tif err == nil {\n\t\tt.Errorf(\"Expected error, got %v\", envMap)\n\t}\n}\n\nfunc TestErrorParsing(t *testing.T) {\n\tenvFileName := \"fixtures/invalid1.env\"\n\tenvMap, err := Read(envFileName)\n\tif err == nil {\n\t\tt.Errorf(\"Expected error, got %v\", envMap)\n\t}\n}\n\nfunc TestComments(t *testing.T) {\n\tenvFileName := \"fixtures/comments.env\"\n\texpectedValues := map[string]string{\n\t\t\"qux\":  \"thud\",\n\t\t\"thud\": \"fred#qux\",\n\t\t\"fred\": \"qux#baz\",\n\t\t\"foo\":  \"bar\",\n\t\t\"bar\":  \"foo#baz\",\n\t\t\"baz\":  \"foo\",\n\t}\n\n\tloadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)\n}\n\nfunc TestWrite(t *testing.T) {\n\twriteAndCompare := func(env string, expected string) {\n\t\tenvMap, _ := Unmarshal(env)\n\t\tactual, _ := Marshal(envMap)\n\t\tif expected != actual {\n\t\t\tt.Errorf(\"Expected '%v' (%v) to write as '%v', got '%v' instead.\", env, envMap, expected, actual)\n\t\t}\n\t}\n\t//just test some single lines to show the general idea\n\t//TestRoundtrip makes most of the good assertions\n\n\t//values are always double-quoted\n\twriteAndCompare(`key=value`, `key=\"value\"`)\n\t//double-quotes are escaped\n\twriteAndCompare(`key=va\"lu\"e`, `key=\"va\\\"lu\\\"e\"`)\n\t//but single quotes are left alone\n\twriteAndCompare(`key=va'lu'e`, `key=\"va'lu'e\"`)\n\t// newlines, backslashes, and some other special chars are escaped\n\twriteAndCompare(`foo=\"\\n\\r\\\\r!\"`, `foo=\"\\n\\r\\\\r\\!\"`)\n\t// lines should be sorted\n\twriteAndCompare(\"foo=bar\\nbaz=buzz\", \"baz=\\\"buzz\\\"\\nfoo=\\\"bar\\\"\")\n\t// integers should not be quoted\n\twriteAndCompare(`key=\"10\"`, `key=10`)\n\n}\n\nfunc TestRoundtrip(t *testing.T) {\n\tfixtures := []string{\"equals.env\", \"exported.env\", \"plain.env\", \"quoted.env\"}\n\tfor _, fixture := range fixtures {\n\t\tfixtureFilename := fmt.Sprintf(\"fixtures/%s\", fixture)\n\t\tenv, err := readFile(fixtureFilename)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Expected '%s' to read without error (%v)\", fixtureFilename, err)\n\t\t}\n\t\trep, err := Marshal(env)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Expected '%s' to Marshal (%v)\", fixtureFilename, err)\n\t\t}\n\t\troundtripped, err := Unmarshal(rep)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Expected '%s' to Mashal and Unmarshal (%v)\", fixtureFilename, err)\n\t\t}\n\t\tif !reflect.DeepEqual(env, roundtripped) {\n\t\t\tt.Errorf(\"Expected '%s' to roundtrip as '%v', got '%v' instead\", fixtureFilename, env, roundtripped)\n\t\t}\n\n\t}\n}\n\nfunc TestTrailingNewlines(t *testing.T) {\n\tcases := map[string]struct {\n\t\tinput string\n\t\tkey   string\n\t\tvalue string\n\t}{\n\t\t\"Simple value without trailing newline\": {\n\t\t\tinput: \"KEY=value\",\n\t\t\tkey:   \"KEY\",\n\t\t\tvalue: \"value\",\n\t\t},\n\t\t\"Value with internal whitespace without trailing newline\": {\n\t\t\tinput: \"KEY=value value\",\n\t\t\tkey:   \"KEY\",\n\t\t\tvalue: \"value value\",\n\t\t},\n\t\t\"Value with internal whitespace with trailing newline\": {\n\t\t\tinput: \"KEY=value value\\n\",\n\t\t\tkey:   \"KEY\",\n\t\t\tvalue: \"value value\",\n\t\t},\n\t\t\"YAML style - value with internal whitespace without trailing newline\": {\n\t\t\tinput: \"KEY: value value\",\n\t\t\tkey:   \"KEY\",\n\t\t\tvalue: \"value value\",\n\t\t},\n\t\t\"YAML style - value with internal whitespace with trailing newline\": {\n\t\t\tinput: \"KEY: value value\\n\",\n\t\t\tkey:   \"KEY\",\n\t\t\tvalue: \"value value\",\n\t\t},\n\t}\n\n\tfor n, c := range cases {\n\t\tt.Run(n, func(t *testing.T) {\n\t\t\tresult, err := Unmarshal(c.input)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Input: %q Unexpected error:\\t%q\", c.input, err)\n\t\t\t}\n\t\t\tif result[c.key] != c.value {\n\t\t\t\tt.Errorf(\"Input %q Expected:\\t %q/%q\\nGot:\\t %q\", c.input, c.key, c.value, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWhitespace(t *testing.T) {\n\tcases := map[string]struct {\n\t\tinput string\n\t\tkey   string\n\t\tvalue string\n\t}{\n\t\t\"Leading whitespace\": {\n\t\t\tinput: \" A=a\\n\",\n\t\t\tkey:   \"A\",\n\t\t\tvalue: \"a\",\n\t\t},\n\t\t\"Leading tab\": {\n\t\t\tinput: \"\\tA=a\\n\",\n\t\t\tkey:   \"A\",\n\t\t\tvalue: \"a\",\n\t\t},\n\t\t\"Leading mixed whitespace\": {\n\t\t\tinput: \" \\t \\t\\n\\t \\t A=a\\n\",\n\t\t\tkey:   \"A\",\n\t\t\tvalue: \"a\",\n\t\t},\n\t\t\"Leading whitespace before export\": {\n\t\t\tinput: \" \\t\\t export    A=a\\n\",\n\t\t\tkey:   \"A\",\n\t\t\tvalue: \"a\",\n\t\t},\n\t\t\"Trailing whitespace\": {\n\t\t\tinput: \"A=a \\t \\t\\n\",\n\t\t\tkey:   \"A\",\n\t\t\tvalue: \"a\",\n\t\t},\n\t\t\"Trailing whitespace with export\": {\n\t\t\tinput: \"export A=a\\t \\t \\n\",\n\t\t\tkey:   \"A\",\n\t\t\tvalue: \"a\",\n\t\t},\n\t\t\"No EOL\": {\n\t\t\tinput: \"A=a\",\n\t\t\tkey:   \"A\",\n\t\t\tvalue: \"a\",\n\t\t},\n\t\t\"Trailing whitespace with no EOL\": {\n\t\t\tinput: \"A=a \",\n\t\t\tkey:   \"A\",\n\t\t\tvalue: \"a\",\n\t\t},\n\t}\n\n\tfor n, c := range cases {\n\t\tt.Run(n, func(t *testing.T) {\n\t\t\tresult, err := Unmarshal(c.input)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Input: %q Unexpected error:\\t%q\", c.input, err)\n\t\t\t}\n\t\t\tif result[c.key] != c.value {\n\t\t\t\tt.Errorf(\"Input %q Expected:\\t %q/%q\\nGot:\\t %q\", c.input, c.key, c.value, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "parser.go",
    "content": "package godotenv\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\t\"unicode\"\n)\n\nconst (\n\tcharComment       = '#'\n\tprefixSingleQuote = '\\''\n\tprefixDoubleQuote = '\"'\n\n\texportPrefix = \"export\"\n)\n\nfunc parseBytes(src []byte, out map[string]string) error {\n\tsrc = bytes.Replace(src, []byte(\"\\r\\n\"), []byte(\"\\n\"), -1)\n\tcutset := src\n\tfor {\n\t\tcutset = getStatementStart(cutset)\n\t\tif cutset == nil {\n\t\t\t// reached end of file\n\t\t\tbreak\n\t\t}\n\n\t\tkey, left, err := locateKeyName(cutset)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvalue, left, err := extractVarValue(left, out)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tout[key] = value\n\t\tcutset = left\n\t}\n\n\treturn nil\n}\n\n// getStatementPosition returns position of statement begin.\n//\n// It skips any comment line or non-whitespace character.\nfunc getStatementStart(src []byte) []byte {\n\tpos := indexOfNonSpaceChar(src)\n\tif pos == -1 {\n\t\treturn nil\n\t}\n\n\tsrc = src[pos:]\n\tif src[0] != charComment {\n\t\treturn src\n\t}\n\n\t// skip comment section\n\tpos = bytes.IndexFunc(src, isCharFunc('\\n'))\n\tif pos == -1 {\n\t\treturn nil\n\t}\n\n\treturn getStatementStart(src[pos:])\n}\n\n// locateKeyName locates and parses key name and returns rest of slice\nfunc locateKeyName(src []byte) (key string, cutset []byte, err error) {\n\t// trim \"export\" and space at beginning\n\tsrc = bytes.TrimLeftFunc(src, isSpace)\n\tif bytes.HasPrefix(src, []byte(exportPrefix)) {\n\t\ttrimmed := bytes.TrimPrefix(src, []byte(exportPrefix))\n\t\tif bytes.IndexFunc(trimmed, isSpace) == 0 {\n\t\t\tsrc = bytes.TrimLeftFunc(trimmed, isSpace)\n\t\t}\n\t}\n\n\t// locate key name end and validate it in single loop\n\toffset := 0\nloop:\n\tfor i, char := range src {\n\t\trchar := rune(char)\n\t\tif isSpace(rchar) {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch char {\n\t\tcase '=', ':':\n\t\t\t// library also supports yaml-style value declaration\n\t\t\tkey = string(src[0:i])\n\t\t\toffset = i + 1\n\t\t\tbreak loop\n\t\tcase '_':\n\t\tdefault:\n\t\t\t// variable name should match [A-Za-z0-9_.]\n\t\t\tif unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treturn \"\", nil, fmt.Errorf(\n\t\t\t\t`unexpected character %q in variable name near %q`,\n\t\t\t\tstring(char), string(src))\n\t\t}\n\t}\n\n\tif len(src) == 0 {\n\t\treturn \"\", nil, errors.New(\"zero length string\")\n\t}\n\n\t// trim whitespace\n\tkey = strings.TrimRightFunc(key, unicode.IsSpace)\n\tcutset = bytes.TrimLeftFunc(src[offset:], isSpace)\n\treturn key, cutset, nil\n}\n\n// extractVarValue extracts variable value and returns rest of slice\nfunc extractVarValue(src []byte, vars map[string]string) (value string, rest []byte, err error) {\n\tquote, hasPrefix := hasQuotePrefix(src)\n\tif !hasPrefix {\n\t\t// unquoted value - read until end of line\n\t\tendOfLine := bytes.IndexFunc(src, isLineEnd)\n\n\t\t// Hit EOF without a trailing newline\n\t\tif endOfLine == -1 {\n\t\t\tendOfLine = len(src)\n\n\t\t\tif endOfLine == 0 {\n\t\t\t\treturn \"\", nil, nil\n\t\t\t}\n\t\t}\n\n\t\t// Convert line to rune away to do accurate countback of runes\n\t\tline := []rune(string(src[0:endOfLine]))\n\n\t\t// Assume end of line is end of var\n\t\tendOfVar := len(line)\n\t\tif endOfVar == 0 {\n\t\t\treturn \"\", src[endOfLine:], nil\n\t\t}\n\n\t\t// Work backwards to check if the line ends in whitespace then\n\t\t// a comment, ie: foo=bar # baz # other\n\t\tfor i := 0; i < endOfVar; i++ {\n\t\t\tif line[i] == charComment && i < endOfVar {\n\t\t\t\tif isSpace(line[i-1]) {\n\t\t\t\t\tendOfVar = i\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttrimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace)\n\n\t\treturn expandVariables(trimmed, vars), src[endOfLine:], nil\n\t}\n\n\t// lookup quoted string terminator\n\tfor i := 1; i < len(src); i++ {\n\t\tif char := src[i]; char != quote {\n\t\t\tcontinue\n\t\t}\n\n\t\t// skip escaped quote symbol (\\\" or \\', depends on quote)\n\t\tif prevChar := src[i-1]; prevChar == '\\\\' {\n\t\t\tcontinue\n\t\t}\n\n\t\t// trim quotes\n\t\ttrimFunc := isCharFunc(rune(quote))\n\t\tvalue = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc))\n\t\tif quote == prefixDoubleQuote {\n\t\t\t// unescape newlines for double quote (this is compat feature)\n\t\t\t// and expand environment variables\n\t\t\tvalue = expandVariables(expandEscapes(value), vars)\n\t\t}\n\n\t\treturn value, src[i+1:], nil\n\t}\n\n\t// return formatted error if quoted string is not terminated\n\tvalEndIndex := bytes.IndexFunc(src, isCharFunc('\\n'))\n\tif valEndIndex == -1 {\n\t\tvalEndIndex = len(src)\n\t}\n\n\treturn \"\", nil, fmt.Errorf(\"unterminated quoted value %s\", src[:valEndIndex])\n}\n\nfunc expandEscapes(str string) string {\n\tout := escapeRegex.ReplaceAllStringFunc(str, func(match string) string {\n\t\tc := strings.TrimPrefix(match, `\\`)\n\t\tswitch c {\n\t\tcase \"n\":\n\t\t\treturn \"\\n\"\n\t\tcase \"r\":\n\t\t\treturn \"\\r\"\n\t\tdefault:\n\t\t\treturn match\n\t\t}\n\t})\n\treturn unescapeCharsRegex.ReplaceAllString(out, \"$1\")\n}\n\nfunc indexOfNonSpaceChar(src []byte) int {\n\treturn bytes.IndexFunc(src, func(r rune) bool {\n\t\treturn !unicode.IsSpace(r)\n\t})\n}\n\n// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character\nfunc hasQuotePrefix(src []byte) (prefix byte, isQuoted bool) {\n\tif len(src) == 0 {\n\t\treturn 0, false\n\t}\n\n\tswitch prefix := src[0]; prefix {\n\tcase prefixDoubleQuote, prefixSingleQuote:\n\t\treturn prefix, true\n\tdefault:\n\t\treturn 0, false\n\t}\n}\n\nfunc isCharFunc(char rune) func(rune) bool {\n\treturn func(v rune) bool {\n\t\treturn v == char\n\t}\n}\n\n// isSpace reports whether the rune is a space character but not line break character\n//\n// this differs from unicode.IsSpace, which also applies line break as space\nfunc isSpace(r rune) bool {\n\tswitch r {\n\tcase '\\t', '\\v', '\\f', '\\r', ' ', 0x85, 0xA0:\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc isLineEnd(r rune) bool {\n\tif r == '\\n' || r == '\\r' {\n\t\treturn true\n\t}\n\treturn false\n}\n\nvar (\n\tescapeRegex        = regexp.MustCompile(`\\\\.`)\n\texpandVarRegex     = regexp.MustCompile(`(\\\\)?(\\$)(\\()?\\{?([A-Z0-9_]+)?\\}?`)\n\tunescapeCharsRegex = regexp.MustCompile(`\\\\([^$])`)\n)\n\nfunc expandVariables(v string, m map[string]string) string {\n\treturn expandVarRegex.ReplaceAllStringFunc(v, func(s string) string {\n\t\tsubmatch := expandVarRegex.FindStringSubmatch(s)\n\n\t\tif submatch == nil {\n\t\t\treturn s\n\t\t}\n\t\tif submatch[1] == \"\\\\\" || submatch[2] == \"(\" {\n\t\t\treturn submatch[0][1:]\n\t\t} else if submatch[4] != \"\" {\n\t\t\tif val, ok := m[submatch[4]]; ok {\n\t\t\t\treturn val\n\t\t\t}\n\t\t\tif val, ok := os.LookupEnv(submatch[4]); ok {\n\t\t\t\treturn val\n\t\t\t}\n\t\t\treturn m[submatch[4]]\n\t\t}\n\t\treturn s\n\t})\n}\n"
  }
]