Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
fail-fast: false
matrix:
go: [ '1.20', '1.19', '1.18', '1.17', '1.16' ]
go: [ '1.25', '1.24', '1.23', '1.22', '1.21' ]
os: [ ubuntu-latest, macOS-latest, windows-latest ]
name: ${{ matrix.os }} Go ${{ matrix.go }} Tests
steps:
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,43 @@ 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.

### Loading All Matching Files

If you want to automatically load all files matching a pattern, you can use `godotenv.LoadAll()` or `godotenv.OverloadAll()`.

By default, these functions will load all files matching the `.env` pattern in the current directory. This includes files like `.env`, `.env.local`, `testing.env`, etc.

```go
// Load all .env files without overwriting existing environment variables
err := godotenv.LoadAll()
if err != nil {
log.Fatal("Error loading .env files")
}
```

You can also provide custom regex patterns to match specific files:

```go
// Load only .env.production and config.* files
err := godotenv.LoadAll(`^\.env\.production$`, `^config\..*`)
if err != nil {
log.Fatal("Error loading env files")
}
```

If you need to overwrite existing environment variables, use `godotenv.OverloadAll()`:

```go
// Load all .env files and overwrite existing environment variables
err := godotenv.OverloadAll()
if err != nil {
log.Fatal("Error loading .env files")
}

// Or with custom patterns
err := godotenv.OverloadAll(`^\.env\.production$`, `^config\..*`)
```

### Command Mode

Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH`
Expand Down
2 changes: 1 addition & 1 deletion autoload/autoload.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ package autoload
import "github.com/joho/godotenv"

func init() {
godotenv.Load()
godotenv.LoadAll()
}
1 change: 1 addition & 0 deletions fixtures/.env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PROD_VAR=prodvalue
1 change: 1 addition & 0 deletions fixtures/.env.testing
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
LOCAL_VAR=localvalue
1 change: 1 addition & 0 deletions fixtures/config.local
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CONFIG_VAR=configvalue
1 change: 1 addition & 0 deletions fixtures/testing.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TEST_VAR=testvalue
89 changes: 89 additions & 0 deletions godotenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ import (
"io"
"os"
"os/exec"
"regexp"
"sort"
"strconv"
"strings"
)

const doubleQuoteSpecialChars = "\\\n\r\"!$`"
const envRegex = `^.*\.env(\..+)?$`

// Parse reads an env file from io.Reader, returning a map of keys and values.
func Parse(r io.Reader) (map[string]string, error) {
Expand Down Expand Up @@ -60,6 +62,26 @@ func Load(filenames ...string) (err error) {
return
}

// LoadAll will read all env files matching the pattern 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 LoadAll without any args it will default to loading all files matching the .env pattern.
// This includes files like .env, .env.testing, testing.env, etc.
//
// You can otherwise provide custom regex patterns to match specific files like:
//
// godotenv.LoadAll(`^\.env\.production$`, `^config\..*`)
//
// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env files to set dev vars or sensible defaults.
func LoadAll(patterns ...string) (err error) {
filenames, err := allFilenames(patterns...)
if err != nil {
return err
}
return Load(filenames...)
}

// 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).
Expand All @@ -83,6 +105,26 @@ func Overload(filenames ...string) (err error) {
return
}

// OverloadAll will read all env files matching the pattern 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 OverloadAll without any args it will default to loading all files matching the .env pattern.
// This includes files like .env, .env.testing, testing.env, etc.
//
// You can otherwise provide custom regex patterns to match specific files like:
//
// godotenv.OverloadAll(`^\.env\.production$`, `^config\..*`)
//
// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env files to forcefully set all vars.
func OverloadAll(patterns ...string) (err error) {
filenames, err := allFilenames(patterns...)
if err != nil {
return err
}
return Overload(filenames...)
}

// 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) {
Expand Down Expand Up @@ -181,6 +223,53 @@ func filenamesOrDefault(filenames []string) []string {
return filenames
}

func allFilenames(patterns ...string) ([]string, error) {
if len(patterns) == 0 {
patterns = []string{envRegex}
}

filenames, err := getEnvFiles(patterns...)
if err != nil {
return nil, err
}

if len(filenames) == 0 {
return []string{".env"}, nil
}

return filenames, nil
}

func getEnvFiles(patterns ...string) ([]string, error) {
entries, err := os.ReadDir(".")
if err != nil {
return nil, err
}

compiledPatterns := make([]*regexp.Regexp, len(patterns))
for i, p := range patterns {
compiledPatterns[i] = regexp.MustCompile(p)
}

var envFiles []string
for _, entry := range entries {
if !entry.IsDir() {
patternMatch(compiledPatterns, entry.Name(), &envFiles)
}
}

return envFiles, nil
}

func patternMatch(compiledPatterns []*regexp.Regexp, filename string, envFiles *[]string) {
for _, pattern := range compiledPatterns {
if pattern.MatchString(filename) {
*envFiles = append(*envFiles, filename)
break
}
}
}

func loadFile(filename string, overload bool) error {
envMap, err := readFile(filename)
if err != nil {
Expand Down
93 changes: 93 additions & 0 deletions godotenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,51 @@ func TestLoadWithNoArgsLoadsDotEnv(t *testing.T) {
}
}

func TestLoadAllWithNoArgsLoadsDotEnv(t *testing.T) {
err := LoadAll()
pathError := err.(*os.PathError)
if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" {
t.Errorf("Didn't try and open .env by default")
}
}

func TestLoadAllWithCustomPattern(t *testing.T) {
os.Clearenv()

originalDir, _ := os.Getwd()
os.Chdir("fixtures")
defer os.Chdir(originalDir)

err := LoadAll(`^\.env\.testing$`)
if err != nil {
t.Errorf("Expected LoadAll to succeed, got error: %v", err)
}

if os.Getenv("LOCAL_VAR") != "localvalue" {
t.Errorf("Expected LOCAL_VAR to be 'localvalue', got '%s'", os.Getenv("LOCAL_VAR"))
}
}

func TestLoadAllWithMultiplePatterns(t *testing.T) {
os.Clearenv()

originalDir, _ := os.Getwd()
os.Chdir("fixtures")
defer os.Chdir(originalDir)

err := LoadAll(`^\.env\.production$`, `^testing\.env$`)
if err != nil {
t.Errorf("Expected LoadAll to succeed, got error: %v", err)
}

if os.Getenv("PROD_VAR") != "prodvalue" {
t.Errorf("Expected PROD_VAR to be 'prodvalue', got '%s'", os.Getenv("PROD_VAR"))
}
if os.Getenv("TEST_VAR") != "testvalue" {
t.Errorf("Expected TEST_VAR to be 'testvalue', got '%s'", os.Getenv("TEST_VAR"))
}
}

func TestOverloadWithNoArgsOverloadsDotEnv(t *testing.T) {
err := Overload()
pathError := err.(*os.PathError)
Expand All @@ -61,6 +106,54 @@ func TestOverloadWithNoArgsOverloadsDotEnv(t *testing.T) {
}
}

func TestOverloadAllWithNoArgsOverloadsDotEnv(t *testing.T) {
err := OverloadAll()
pathError := err.(*os.PathError)
if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" {
t.Errorf("Didn't try and open .env by default")
}
}

func TestOverloadAllWithCustomPattern(t *testing.T) {
os.Clearenv()
os.Setenv("LOCAL_VAR", "original")

originalDir, _ := os.Getwd()
os.Chdir("fixtures")
defer os.Chdir(originalDir)

err := OverloadAll(`^\.env\.testing$`)
if err != nil {
t.Errorf("Expected OverloadAll to succeed, got error: %v", err)
}

if os.Getenv("LOCAL_VAR") != "localvalue" {
t.Errorf("Expected LOCAL_VAR to be overloaded to 'localvalue', got '%s'", os.Getenv("LOCAL_VAR"))
}
}

func TestOverloadAllWithMultiplePatterns(t *testing.T) {
os.Clearenv()
os.Setenv("PROD_VAR", "original_prod")
os.Setenv("TEST_VAR", "original_test")

originalDir, _ := os.Getwd()
os.Chdir("fixtures")
defer os.Chdir(originalDir)

err := OverloadAll(`^\.env\.production$`, `^testing\.env$`)
if err != nil {
t.Errorf("Expected OverloadAll to succeed, got error: %v", err)
}

if os.Getenv("PROD_VAR") != "prodvalue" {
t.Errorf("Expected PROD_VAR to be overloaded to 'prodvalue', got '%s'", os.Getenv("PROD_VAR"))
}
if os.Getenv("TEST_VAR") != "testvalue" {
t.Errorf("Expected TEST_VAR to be overloaded to 'testvalue', got '%s'", os.Getenv("TEST_VAR"))
}
}

func TestLoadFileNotFound(t *testing.T) {
err := Load("somefilethatwillneverexistever.env")
if err == nil {
Expand Down
Loading