Repository: joho/godotenv
Branch: main
Commit: a2be92d182fc
Files: 20
Total size: 41.8 KB
Directory structure:
gitextract_kzep8nnw/
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── ci.yml
│ ├── codeql-analysis.yml
│ └── release.yml
├── .gitignore
├── LICENCE
├── README.md
├── autoload/
│ └── autoload.go
├── cmd/
│ └── godotenv/
│ └── cmd.go
├── fixtures/
│ ├── comments.env
│ ├── equals.env
│ ├── exported.env
│ ├── invalid1.env
│ ├── plain.env
│ ├── quoted.env
│ └── substitutions.env
├── go.mod
├── godotenv.go
├── godotenv_test.go
└── parser.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "gomod"
directory: /
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
================================================
FILE: .github/workflows/ci.yml
================================================
name: 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@v4
with:
go-version: ${{ matrix.go }}
- run: go test
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# 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
================================================
FILE: .github/workflows/release.yml
================================================
on:
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
================================================
FILE: .gitignore
================================================
.DS_Store
================================================
FILE: LICENCE
================================================
Copyright (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.
================================================
FILE: README.md
================================================
# 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.
================================================
FILE: autoload/autoload.go
================================================
package 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()
}
================================================
FILE: cmd/godotenv/cmd.go
================================================
package 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)
}
}
================================================
FILE: fixtures/comments.env
================================================
# Full line comment
qux=thud # fred # other
thud=fred#qux # other
fred=qux#baz # other # more
foo=bar # baz
bar=foo#baz
baz="foo"#bar
================================================
FILE: fixtures/equals.env
================================================
export OPTION_A='postgres://localhost:5432/database?sslmode=disable'
================================================
FILE: fixtures/exported.env
================================================
export OPTION_A=2
export OPTION_B='\n'
================================================
FILE: fixtures/invalid1.env
================================================
INVALID LINE
foo=bar
================================================
FILE: fixtures/plain.env
================================================
OPTION_A=1
OPTION_B=2
OPTION_C= 3
OPTION_D =4
OPTION_E = 5
OPTION_F =
OPTION_G=
OPTION_H=1 2
================================================
FILE: fixtures/quoted.env
================================================
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
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"
================================================
FILE: fixtures/substitutions.env
================================================
OPTION_A=1
OPTION_B=${OPTION_A}
OPTION_C=$OPTION_B
OPTION_D=${OPTION_A}${OPTION_B}
OPTION_E=${OPTION_NOT_DEFINED}
OPTION_F=${GLOBAL_OPTION}
================================================
FILE: go.mod
================================================
module github.com/joho/godotenv
go 1.12
================================================
FILE: godotenv.go
================================================
// 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
}
================================================
FILE: godotenv_test.go
================================================
package 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"
presets := map[string]string{
"GLOBAL_OPTION": "global",
}
expectedValues := map[string]string{
"OPTION_A": "1",
"OPTION_B": "1",
"OPTION_C": "1",
"OPTION_D": "11",
"OPTION_E": "",
"OPTION_F": "global",
}
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, presets)
}
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{
"qux": "thud",
"thud": "fred#qux",
"fred": "qux#baz",
"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)
}
})
}
}
func TestWhitespace(t *testing.T) {
cases := map[string]struct {
input string
key string
value string
}{
"Leading whitespace": {
input: " A=a\n",
key: "A",
value: "a",
},
"Leading tab": {
input: "\tA=a\n",
key: "A",
value: "a",
},
"Leading mixed whitespace": {
input: " \t \t\n\t \t A=a\n",
key: "A",
value: "a",
},
"Leading whitespace before export": {
input: " \t\t export A=a\n",
key: "A",
value: "a",
},
"Trailing whitespace": {
input: "A=a \t \t\n",
key: "A",
value: "a",
},
"Trailing whitespace with export": {
input: "export A=a\t \t \n",
key: "A",
value: "a",
},
"No EOL": {
input: "A=a",
key: "A",
value: "a",
},
"Trailing whitespace with no EOL": {
input: "A=a ",
key: "A",
value: "a",
},
}
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)
}
})
}
}
================================================
FILE: parser.go
================================================
package godotenv
import (
"bytes"
"errors"
"fmt"
"os"
"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: foo=bar # baz # other
for i := 0; i < endOfVar; i++ {
if line[i] == charComment && i < endOfVar {
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, isQuoted 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] != "" {
if val, ok := m[submatch[4]]; ok {
return val
}
if val, ok := os.LookupEnv(submatch[4]); ok {
return val
}
return m[submatch[4]]
}
return s
})
}
gitextract_kzep8nnw/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ ├── codeql-analysis.yml │ └── release.yml ├── .gitignore ├── LICENCE ├── README.md ├── autoload/ │ └── autoload.go ├── cmd/ │ └── godotenv/ │ └── cmd.go ├── fixtures/ │ ├── comments.env │ ├── equals.env │ ├── exported.env │ ├── invalid1.env │ ├── plain.env │ ├── quoted.env │ └── substitutions.env ├── go.mod ├── godotenv.go ├── godotenv_test.go └── parser.go
SYMBOL INDEX (58 symbols across 5 files)
FILE: autoload/autoload.go
function init (line 13) | func init() {
FILE: cmd/godotenv/cmd.go
function main (line 12) | func main() {
FILE: godotenv.go
constant doubleQuoteSpecialChars (line 27) | doubleQuoteSpecialChars = "\\\n\r\"!$`"
function Parse (line 30) | func Parse(r io.Reader) (map[string]string, error) {
function Load (line 51) | func Load(filenames ...string) (err error) {
function Overload (line 74) | func Overload(filenames ...string) (err error) {
function Read (line 88) | func Read(filenames ...string) (envMap map[string]string, err error) {
function Unmarshal (line 109) | func Unmarshal(str string) (envMap map[string]string, err error) {
function UnmarshalBytes (line 114) | func UnmarshalBytes(src []byte) (map[string]string, error) {
function Exec (line 128) | func Exec(filenames []string, cmd string, cmdArgs []string, overload boo...
function Write (line 145) | func Write(envMap map[string]string, filename string) error {
function Marshal (line 164) | func Marshal(envMap map[string]string) (string, error) {
function filenamesOrDefault (line 177) | func filenamesOrDefault(filenames []string) []string {
function loadFile (line 184) | func loadFile(filename string, overload bool) error {
function readFile (line 206) | func readFile(filename string) (envMap map[string]string, err error) {
function doubleQuoteEscape (line 216) | func doubleQuoteEscape(line string) string {
FILE: godotenv_test.go
function parseAndCompare (line 14) | func parseAndCompare(t *testing.T, rawEnvLine string, expectedKey string...
function loadEnvAndCompareValues (line 26) | func loadEnvAndCompareValues(t *testing.T, loader func(files ...string) ...
function TestLoadWithNoArgsLoadsDotEnv (line 48) | func TestLoadWithNoArgsLoadsDotEnv(t *testing.T) {
function TestOverloadWithNoArgsOverloadsDotEnv (line 56) | func TestOverloadWithNoArgsOverloadsDotEnv(t *testing.T) {
function TestLoadFileNotFound (line 64) | func TestLoadFileNotFound(t *testing.T) {
function TestOverloadFileNotFound (line 71) | func TestOverloadFileNotFound(t *testing.T) {
function TestReadPlainEnv (line 78) | func TestReadPlainEnv(t *testing.T) {
function TestParse (line 107) | func TestParse(t *testing.T) {
function TestLoadDoesNotOverride (line 124) | func TestLoadDoesNotOverride(t *testing.T) {
function TestOverloadDoesOverride (line 140) | func TestOverloadDoesOverride(t *testing.T) {
function TestLoadPlainEnv (line 154) | func TestLoadPlainEnv(t *testing.T) {
function TestLoadExportedEnv (line 168) | func TestLoadExportedEnv(t *testing.T) {
function TestLoadEqualsEnv (line 178) | func TestLoadEqualsEnv(t *testing.T) {
function TestLoadQuotedEnv (line 187) | func TestLoadQuotedEnv(t *testing.T) {
function TestSubstitutions (line 208) | func TestSubstitutions(t *testing.T) {
function TestExpanding (line 227) | func TestExpanding(t *testing.T) {
function TestVariableStringValueSeparator (line 290) | func TestVariableStringValueSeparator(t *testing.T) {
function TestActualEnvVarsAreLeftAlone (line 318) | func TestActualEnvVarsAreLeftAlone(t *testing.T) {
function TestParsing (line 328) | func TestParsing(t *testing.T) {
function TestLinesToIgnore (line 424) | func TestLinesToIgnore(t *testing.T) {
function TestErrorReadDirectory (line 460) | func TestErrorReadDirectory(t *testing.T) {
function TestErrorParsing (line 469) | func TestErrorParsing(t *testing.T) {
function TestComments (line 477) | func TestComments(t *testing.T) {
function TestWrite (line 491) | func TestWrite(t *testing.T) {
function TestRoundtrip (line 517) | func TestRoundtrip(t *testing.T) {
function TestTrailingNewlines (line 540) | func TestTrailingNewlines(t *testing.T) {
function TestWhitespace (line 586) | func TestWhitespace(t *testing.T) {
FILE: parser.go
constant charComment (line 14) | charComment = '#'
constant prefixSingleQuote (line 15) | prefixSingleQuote = '\''
constant prefixDoubleQuote (line 16) | prefixDoubleQuote = '"'
constant exportPrefix (line 18) | exportPrefix = "export"
function parseBytes (line 21) | func parseBytes(src []byte, out map[string]string) error {
function getStatementStart (line 51) | func getStatementStart(src []byte) []byte {
function locateKeyName (line 72) | func locateKeyName(src []byte) (key string, cutset []byte, err error) {
function extractVarValue (line 121) | func extractVarValue(src []byte, vars map[string]string) (value string, ...
function expandEscapes (line 193) | func expandEscapes(str string) string {
function indexOfNonSpaceChar (line 208) | func indexOfNonSpaceChar(src []byte) int {
function hasQuotePrefix (line 215) | func hasQuotePrefix(src []byte) (prefix byte, isQuoted bool) {
function isCharFunc (line 228) | func isCharFunc(char rune) func(rune) bool {
function isSpace (line 237) | func isSpace(r rune) bool {
function isLineEnd (line 245) | func isLineEnd(r rune) bool {
function expandVariables (line 258) | func expandVariables(v string, m map[string]string) string {
Condensed preview — 20 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (48K chars).
[
{
"path": ".github/dependabot.yml",
"chars": 202,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"gomod\"\n directory: /\n schedule:\n interval: \"daily\"\n - package-ec"
},
{
"path": ".github/workflows/ci.yml",
"chars": 461,
"preview": "name: CI\n\non: [push]\n\njobs:\n test:\n runs-on: ${{ matrix.os }}\n strategy:\n fail-fast: false\n matrix:\n "
},
{
"path": ".github/workflows/codeql-analysis.yml",
"chars": 2716,
"preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
},
{
"path": ".github/workflows/release.yml",
"chars": 873,
"preview": "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"
},
{
"path": ".gitignore",
"chars": 10,
"preview": ".DS_Store\n"
},
{
"path": "LICENCE",
"chars": 1069,
"preview": "Copyright (c) 2013 John Barton\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na cop"
},
{
"path": "README.md",
"chars": 6122,
"preview": "# GoDotEnv  [\n\nfunc main() {\n\tvar showHelp bool"
},
{
"path": "fixtures/comments.env",
"chars": 134,
"preview": "# Full line comment\nqux=thud # fred # other\nthud=fred#qux # other\nfred=qux#baz # other # more\nfoo=bar # baz\nbar=foo#baz\n"
},
{
"path": "fixtures/equals.env",
"chars": 69,
"preview": "export OPTION_A='postgres://localhost:5432/database?sslmode=disable'\n"
},
{
"path": "fixtures/exported.env",
"chars": 39,
"preview": "export OPTION_A=2\nexport OPTION_B='\\n'\n"
},
{
"path": "fixtures/invalid1.env",
"chars": 21,
"preview": "INVALID LINE\nfoo=bar\n"
},
{
"path": "fixtures/plain.env",
"chars": 93,
"preview": "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",
"chars": 284,
"preview": "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"
},
{
"path": "fixtures/substitutions.env",
"chars": 140,
"preview": "OPTION_A=1\nOPTION_B=${OPTION_A}\nOPTION_C=$OPTION_B\nOPTION_D=${OPTION_A}${OPTION_B}\nOPTION_E=${OPTION_NOT_DEFINED}\nOPTION"
},
{
"path": "go.mod",
"chars": 41,
"preview": "module github.com/joho/godotenv\n\ngo 1.12\n"
},
{
"path": "godotenv.go",
"chars": 6020,
"preview": "// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv)\n//\n// Examples/readme c"
},
{
"path": "godotenv_test.go",
"chars": 16880,
"preview": "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]s"
},
{
"path": "parser.go",
"chars": 6277,
"preview": "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 "
}
]
About this extraction
This page contains the full source code of the joho/godotenv GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 20 files (41.8 KB), approximately 12.8k tokens, and a symbol index with 58 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.