-
Notifications
You must be signed in to change notification settings - Fork 433
Inherit variables from environment #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
d3051d5
e417b72
c787b07
48ccd66
6f27d4e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
VARIABLE_NOT_FOUND |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
PATH |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,7 +28,9 @@ import ( | |
|
||
const doubleQuoteSpecialChars = "\\\n\r\"!$`" | ||
|
||
// Load will read your env file(s) and load them into ENV for this process. | ||
// LoadWithLookupFn will read your env file(s), lookup the inherited variables with `lookupFn` | ||
// | ||
// and load them into ENV for this process. | ||
// | ||
// Call this function as close as possible to the start of your program (ideally in main) | ||
// | ||
|
@@ -39,18 +41,35 @@ const doubleQuoteSpecialChars = "\\\n\r\"!$`" | |
// 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) { | ||
func LoadWithLookupFn(lookupFn func(string) (string, bool), filenames ...string) (err error) { | ||
filenames = filenamesOrDefault(filenames) | ||
|
||
for _, filename := range filenames { | ||
err = loadFile(filename, false) | ||
err = loadFile(filename, false, lookupFn) | ||
if err != nil { | ||
return // return early on a spazout | ||
} | ||
} | ||
return | ||
} | ||
|
||
// 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) { | ||
return LoadWithLookupFn(func(string) (string, bool){ | ||
return "", true | ||
}, 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) | ||
|
@@ -66,7 +85,9 @@ func Overload(filenames ...string) (err error) { | |
filenames = filenamesOrDefault(filenames) | ||
|
||
for _, filename := range filenames { | ||
err = loadFile(filename, true) | ||
err = loadFile(filename, true, func(string) (string, bool) { | ||
return "", true | ||
}) | ||
if err != nil { | ||
return // return early on a spazout | ||
} | ||
|
@@ -81,7 +102,7 @@ func Read(filenames ...string) (envMap map[string]string, err error) { | |
envMap = make(map[string]string) | ||
|
||
for _, filename := range filenames { | ||
individualEnvMap, individualErr := readFile(filename) | ||
individualEnvMap, individualErr := readFile(filename, noLookupFn) | ||
|
||
if individualErr != nil { | ||
err = individualErr | ||
|
@@ -96,32 +117,61 @@ func Read(filenames ...string) (envMap map[string]string, err error) { | |
return | ||
} | ||
|
||
// Parse reads an env file from io.Reader, returning a map of keys and values. | ||
func Parse(r io.Reader) (envMap map[string]string, err error) { | ||
// ReadWithLookup all env (with same file loading semantics as Load) but return values as | ||
// a map rather than automatically writing values into env | ||
func ReadWithLookup(lookupFn func(string)(string, bool), filenames ...string) (envMap map[string]string, err error) { | ||
filenames = filenamesOrDefault(filenames) | ||
envMap = make(map[string]string) | ||
|
||
for _, filename := range filenames { | ||
individualEnvMap, individualErr := readFile(filename, lookupFn) | ||
|
||
if individualErr != nil { | ||
err = individualErr | ||
return // return early on a spazout | ||
} | ||
|
||
for key, value := range individualEnvMap { | ||
envMap[key] = value | ||
} | ||
} | ||
|
||
return | ||
} | ||
|
||
// ParseWithLookup reads an env file from io.Reader resolving variables with lookupFn, returning a map of keys and values. | ||
func ParseWithLookup(r io.Reader, lookupFn func(string)(string, bool)) (map[string]string, error) { | ||
envMap := make(map[string]string) | ||
|
||
var lines []string | ||
scanner := bufio.NewScanner(r) | ||
for scanner.Scan() { | ||
lines = append(lines, scanner.Text()) | ||
} | ||
|
||
if err = scanner.Err(); err != nil { | ||
return | ||
if err := scanner.Err(); err != nil { | ||
return envMap, err | ||
} | ||
|
||
for _, fullLine := range lines { | ||
if !isIgnoredLine(fullLine) { | ||
var key, value string | ||
key, value, err = parseLine(fullLine, envMap) | ||
|
||
key, value, err := parseLine(fullLine, envMap, lookupFn) | ||
if err != nil { | ||
return | ||
return envMap, err | ||
} | ||
if key == "" { | ||
continue | ||
} | ||
envMap[key] = value | ||
} | ||
} | ||
return | ||
return envMap, nil | ||
} | ||
|
||
// Parse reads an env file from io.Reader, returning a map of keys and values. | ||
func Parse(r io.Reader) (map[string]string, error) { | ||
return ParseWithLookup(r, noLookupFn) | ||
ulyssessouza marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
//Unmarshal reads an env file from a string, returning a map of keys and values. | ||
|
@@ -187,8 +237,8 @@ func filenamesOrDefault(filenames []string) []string { | |
return filenames | ||
} | ||
|
||
func loadFile(filename string, overload bool) error { | ||
envMap, err := readFile(filename) | ||
func loadFile(filename string, overload bool, lookupFn func(string)(string, bool)) error { | ||
envMap, err := readFile(filename, lookupFn) | ||
if err != nil { | ||
return err | ||
} | ||
|
@@ -209,19 +259,19 @@ func loadFile(filename string, overload bool) error { | |
return nil | ||
} | ||
|
||
func readFile(filename string) (envMap map[string]string, err error) { | ||
func readFile(filename string, lookupFn func(string)(string, bool)) (envMap map[string]string, err error) { | ||
file, err := os.Open(filename) | ||
if err != nil { | ||
return | ||
} | ||
defer file.Close() | ||
|
||
return Parse(file) | ||
return ParseWithLookup(file, lookupFn) | ||
} | ||
|
||
var exportRegex = regexp.MustCompile(`^\s*(?:export\s+)?(.*?)\s*$`) | ||
|
||
func parseLine(line string, envMap map[string]string) (key string, value string, err error) { | ||
func parseLine(line string, envMap map[string]string, lookupFn func(string)(string, bool)) (key string, value string, err error) { | ||
if len(line) == 0 { | ||
err = errors.New("zero length string") | ||
return | ||
|
@@ -258,30 +308,49 @@ func parseLine(line string, envMap map[string]string) (key string, value string, | |
splitString = strings.SplitN(line, ":", 2) | ||
} | ||
|
||
if len(splitString) != 2 { | ||
err = errors.New("Can't separate key from value") | ||
// Parse the key | ||
key = strings.TrimSpace(strings.TrimPrefix(splitString[0], "export ")) | ||
key = exportRegex.ReplaceAllString(key, "$1") | ||
if err = validateVariableName(key); err != nil { | ||
return | ||
} | ||
|
||
// Parse the key | ||
key = splitString[0] | ||
if strings.HasPrefix(key, "export") { | ||
key = strings.TrimPrefix(key, "export") | ||
// Environment inherited variable | ||
if firstEquals < 0 && firstColon < 0 { | ||
value = "" | ||
v, ok := lookupFn(strings.TrimSpace(key)) | ||
if ok { | ||
value = v | ||
} | ||
return | ||
} | ||
key = strings.TrimSpace(key) | ||
|
||
key = exportRegex.ReplaceAllString(splitString[0], "$1") | ||
if len(splitString) != 2 { | ||
err = errors.New("Can't separate key from value") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. errors should not start with a capital; perhaps change "Can't" to "failed to" |
||
return | ||
} | ||
|
||
// Parse the value | ||
value = parseValue(splitString[1], envMap) | ||
return | ||
} | ||
|
||
func validateVariableName(key string) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did we actually need a validation for this? Note that there's no strict rules for variable names; this regex would also exclude Windows We made this mistake in the Docker Engine, and had to revert; moby/moby#16608 Generally, I would consider the variable name to be:
To be a "valid" variable. Code that consumes this library can decide what to do with those (if anything); eg for docker we pass any variable to the runtime, and have the kernel/shell decide if it's accepted. |
||
key = strings.TrimSpace(strings.TrimPrefix(key, "export ")) | ||
if !variableNameRegex.MatchString(key) { | ||
return fmt.Errorf("invalid variable name %q", key) | ||
} | ||
return nil | ||
} | ||
|
||
var ( | ||
singleQuotesRegex = regexp.MustCompile(`\A'(.*)'\z`) | ||
doubleQuotesRegex = regexp.MustCompile(`\A"(.*)"\z`) | ||
escapeRegex = regexp.MustCompile(`\\.`) | ||
unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) | ||
variableNameRegex = regexp.MustCompile(`^[_\\.a-zA-Z0-9]+$`) | ||
|
||
noLookupFn = func(string)(string, bool) {return "", true} | ||
) | ||
|
||
func parseValue(value string, envMap map[string]string) string { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this one have used
noLookupFn
as well?But perhaps instead of a dummy function, pass
nil
as lookup-function, then where it's executed, check if it's nil and if so, skip executing?