From 916696a1b40779a2fc2b6bf8e7df35520b7b92db Mon Sep 17 00:00:00 2001 From: jfwittmann Date: Sat, 21 May 2022 14:19:24 +0200 Subject: [PATCH 1/4] - Change the order in which the environment variables from the .env file are merged with the OS environment variables. Variables from the .env file can override OS environment variables. - Add the possibility within the .env file to refer to variables within the .env file or to replace the placeholder variable with the value from the OS environment variable. --- .env.demo | 3 +++ .gitignore | 1 + main.go | 29 +++++++++++++++++++++++++---- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/.env.demo b/.env.demo index d485f46..356174a 100644 --- a/.env.demo +++ b/.env.demo @@ -7,3 +7,6 @@ DOT.TED=dotted SHARP=sharp after a var # is not a comment # This is a most likely override SHELL=invalid +YOUR_NAME=Felix +GREETING=Hello ${YOUR_NAME}! +TEST_HOME_PATH=${HOME} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 66fd13c..a319b52 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ # Dependency directories (remove the comment below to include it) # vendor/ +.vscode/settings.json diff --git a/main.go b/main.go index bf16c74..48c3714 100644 --- a/main.go +++ b/main.go @@ -18,12 +18,14 @@ const ( CommentRx = `^[\s]*#` // NameRx is much tighter than Posix, which accepts anything but NUL and '=', // but laxer than shells, which do not accept dots. Names are assumed to be pre-trimmed. - NameRx = `^[[:alpha:]][-._a-zA-Z0-9]*` + NameRx = `^[[:alpha:]][-._a-zA-Z0-9]*` + EnvKeyReplaceRx = `\$\{[^}]+\}` ) var ( - commentRx = regexp.MustCompile(CommentRx) - nameRx = regexp.MustCompile(NameRx) + commentRx = regexp.MustCompile(CommentRx) + nameRx = regexp.MustCompile(NameRx) + envKeyReplaceRx = regexp.MustCompile(EnvKeyReplaceRx) ) type env map[string]string @@ -76,6 +78,23 @@ func (e env) Merge(f env) env { return res } +// replaces ${ENV_KEY} placeholder in env values +func (e env) ReplaceEnvKeys() env { + res := make(env, len(e)) + for k, v := range e { + res[k] = envKeyReplaceRx.ReplaceAllStringFunc(v, func(part string) string { + plen := len(part) + if plen > 3 { + envKey := part[2 : plen-1] + envPart := e[envKey] + return envPart + } + return part + }) + } + return res +} + func readCloser(args []string) (io.ReadCloser, *flag.FlagSet, error) { if len(args) < 2 { return nil, nil, errors.New("need at least a command to run") @@ -138,7 +157,9 @@ func main() { }() env := envFromReader(rc) - env = env.Merge(envFromEnv()) + osEnv := envFromEnv() + env = osEnv.Merge(env) + env = env.ReplaceEnvKeys() toRun := fs.Args() // Length checked during readCloser(). name := toRun[0] From 69558217f03fe16bbc0536076bada857211a06ee Mon Sep 17 00:00:00 2001 From: jfwittmann Date: Sat, 21 May 2022 16:09:56 +0200 Subject: [PATCH 2/4] Improvement, log if the variable placeholder does not exist in the environment variables or if the placeholder is empty. --- Makefile | 2 ++ main.go | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 2e609d9..189ccf8 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,4 @@ demo: LOCAL=demo go run . -f .env.demo env | sort +demo-convert: + LOCAL=demo2 go run . -f .env.demo env | sort | sed -E 's/(.*)/-e \1/' | tr '\n' ' ' \ No newline at end of file diff --git a/main.go b/main.go index 48c3714..4e3d978 100644 --- a/main.go +++ b/main.go @@ -86,9 +86,14 @@ func (e env) ReplaceEnvKeys() env { plen := len(part) if plen > 3 { envKey := part[2 : plen-1] - envPart := e[envKey] - return envPart + if envPart, ok := e[envKey]; ok { + return envPart + } else { + log.Printf(`Replacing the variable failed, env key is unknown: "%s"`, part) + } + return part } + log.Printf(`Replacing the variable failed, env key is empty: "%s"`, part) return part }) } From ea2e3b1e6cf7c817460b342fca8dfca60da9fabf Mon Sep 17 00:00:00 2001 From: jfwittmann Date: Tue, 24 May 2022 21:52:49 +0200 Subject: [PATCH 3/4] add -o flag and support default values --- .env.demo | 4 +++- .gitignore | 2 +- Makefile | 4 +++- main.go | 68 ++++++++++++++++++++++++++++++++++++++++-------------- 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/.env.demo b/.env.demo index 356174a..a990266 100644 --- a/.env.demo +++ b/.env.demo @@ -9,4 +9,6 @@ SHARP=sharp after a var # is not a comment SHELL=invalid YOUR_NAME=Felix GREETING=Hello ${YOUR_NAME}! -TEST_HOME_PATH=${HOME} \ No newline at end of file +# Test of the -o Flag, the current directory should be at the beginning of the path +PATH=.:${PATH} +TEST_DEFAULT=Hello ${UNKNOWN_NAME|Peter}! \ No newline at end of file diff --git a/.gitignore b/.gitignore index a319b52..35e523e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,4 @@ # Dependency directories (remove the comment below to include it) # vendor/ -.vscode/settings.json +.vscode/* diff --git a/Makefile b/Makefile index 189ccf8..c7881c5 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,6 @@ demo: LOCAL=demo go run . -f .env.demo env | sort demo-convert: - LOCAL=demo2 go run . -f .env.demo env | sort | sed -E 's/(.*)/-e \1/' | tr '\n' ' ' \ No newline at end of file + LOCAL=demo2 go run . -f .env.demo env | sort | sed -E 's/(.*)/-e \1/' | tr '\n' ' ' +demo-override: + LOCAL=demo2 go run . -o -f .env.demo env | grep PATH \ No newline at end of file diff --git a/main.go b/main.go index 4e3d978..e8e901f 100644 --- a/main.go +++ b/main.go @@ -20,12 +20,14 @@ const ( // but laxer than shells, which do not accept dots. Names are assumed to be pre-trimmed. NameRx = `^[[:alpha:]][-._a-zA-Z0-9]*` EnvKeyReplaceRx = `\$\{[^}]+\}` + VarAndDefaultRx = `([^|]+)(?:\|(.+))?` ) var ( commentRx = regexp.MustCompile(CommentRx) nameRx = regexp.MustCompile(NameRx) envKeyReplaceRx = regexp.MustCompile(EnvKeyReplaceRx) + varAndDefaultRx = regexp.MustCompile(VarAndDefaultRx) ) type env map[string]string @@ -78,45 +80,66 @@ func (e env) Merge(f env) env { return res } -// replaces ${ENV_KEY} placeholder in env values -func (e env) ReplaceEnvKeys() env { +// replaces ${ENV_KEY} keys in env with source env values +func (e env) ReplaceEnvKeys(source env) env { res := make(env, len(e)) for k, v := range e { res[k] = envKeyReplaceRx.ReplaceAllStringFunc(v, func(part string) string { plen := len(part) if plen > 3 { - envKey := part[2 : plen-1] - if envPart, ok := e[envKey]; ok { + envKeyAndDefault := part[2 : plen-1] + mr := varAndDefaultRx.FindStringSubmatch(envKeyAndDefault) + envKey := mr[1] + if envPart, ok := source[envKey]; ok { return envPart + } + // default value? + if len(mr) > 2 && mr[2] != "" { + return mr[2] } else { - log.Printf(`Replacing the variable failed, env key is unknown: "%s"`, part) + log.Printf(`Replace variable failed, env key is unknown and no default value was defined: "%s"`, part) + return part } + } else { + log.Printf(`Replacing the variable failed, env key is empty: "%s"`, part) return part } - log.Printf(`Replacing the variable failed, env key is empty: "%s"`, part) - return part }) } return res } -func readCloser(args []string) (io.ReadCloser, *flag.FlagSet, error) { +type options = map[string]any + +func parseArgs(args []string) (options, *flag.FlagSet, error) { + opts := make(map[string]any) if len(args) < 2 { return nil, nil, errors.New("need at least a command to run") } + fs := flag.NewFlagSet(args[0], flag.ContinueOnError) - inName := fs.String("f", ".env", "The file from which to read the environment variables") + inFile := fs.String("f", ".env", "The file from which to read the environment variables") + overrideEnv := fs.Bool("o", false, "Do override existing environment variables with own values.\n"+ + "But you can still use the original environment variable names as variables inside the .env file.") if err := fs.Parse(args[1:]); err != nil { - return nil, nil, fmt.Errorf("failed parsing flags: %w", err) + return opts, nil, fmt.Errorf("failed parsing flags: %w", err) } + opts[""] = *overrideEnv + opts[""] = *inFile + if len(fs.Args()) == 0 { - return nil, nil, errors.New("no command to run") + return opts, nil, errors.New("no command to run") } - inFile, err := os.Open(*inName) + + return opts, fs, nil +} + +func readCloser(opts options) (io.ReadCloser, error) { + inFile, err := os.Open(opts[""].(string)) if err != nil { - return nil, fs, fmt.Errorf("failed reading %s: %v", *inName, err) + return nil, fmt.Errorf("failed reading %s: %v", opts[""], err) } - return inFile, fs, nil + return inFile, nil } func run(env env, name string, args []string) error { @@ -150,7 +173,12 @@ func main() { err error exitCode int ) - rc, fs, err := readCloser(os.Args) + + opts, fs, err := parseArgs(os.Args) + if err != nil { + log.Fatal(err) + } + rc, err := readCloser(opts) if err != nil { log.Fatal(err) } @@ -163,8 +191,13 @@ func main() { env := envFromReader(rc) osEnv := envFromEnv() - env = osEnv.Merge(env) - env = env.ReplaceEnvKeys() + if opts[""].(bool) { + env = env.ReplaceEnvKeys(osEnv) + env = osEnv.Merge(env) + } else { + env = env.Merge(osEnv) + env = env.ReplaceEnvKeys(env) + } toRun := fs.Args() // Length checked during readCloser(). name := toRun[0] @@ -173,6 +206,7 @@ func main() { } exit, ok := err.(*exec.ExitError) if !ok { + log.Printf("non-exit error running %s: %v", name, err) exitCode = 1 } From 9fa42eec425f03a75d3aba65bd54877fe75e0cff Mon Sep 17 00:00:00 2001 From: jfwittmann Date: Tue, 24 May 2022 22:08:26 +0200 Subject: [PATCH 4/4] cleanup, move the options type to the head of the source file --- main.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main.go b/main.go index e8e901f..8aef5a0 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,7 @@ var ( varAndDefaultRx = regexp.MustCompile(VarAndDefaultRx) ) +type options = map[string]any type env map[string]string func envFromEnv() env { @@ -109,8 +110,6 @@ func (e env) ReplaceEnvKeys(source env) env { return res } -type options = map[string]any - func parseArgs(args []string) (options, *flag.FlagSet, error) { opts := make(map[string]any) if len(args) < 2 {