Skip to content
Open
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
5 changes: 5 additions & 0 deletions .env.demo
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ 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 of the -o Flag, the current directory should be at the beginning of the path
PATH=.:${PATH}
TEST_DEFAULT=Hello ${UNKNOWN_NAME|Peter}!
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@

# Dependency directories (remove the comment below to include it)
# vendor/
.vscode/*
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,2 +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' ' '
demo-override:
LOCAL=demo2 go run . -o -f .env.demo env | grep PATH
83 changes: 71 additions & 12 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@ 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 = `\$\{[^}]+\}`
VarAndDefaultRx = `([^|]+)(?:\|(.+))?`
)

var (
commentRx = regexp.MustCompile(CommentRx)
nameRx = regexp.MustCompile(NameRx)
commentRx = regexp.MustCompile(CommentRx)
nameRx = regexp.MustCompile(NameRx)
envKeyReplaceRx = regexp.MustCompile(EnvKeyReplaceRx)
varAndDefaultRx = regexp.MustCompile(VarAndDefaultRx)
)

type options = map[string]any
type env map[string]string

func envFromEnv() env {
Expand Down Expand Up @@ -76,23 +81,64 @@ func (e env) Merge(f env) env {
return res
}

func readCloser(args []string) (io.ReadCloser, *flag.FlagSet, error) {
// 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 {
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(`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
}
})
}
return res
}

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["<override_envs>"] = *overrideEnv
opts["<env_file_name>"] = *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["<env_file_name>"].(string))
if err != nil {
return nil, fs, fmt.Errorf("failed reading %s: %v", *inName, err)
return nil, fmt.Errorf("failed reading %s: %v", opts["<env_file_name>"], err)
}
return inFile, fs, nil
return inFile, nil
}

func run(env env, name string, args []string) error {
Expand Down Expand Up @@ -126,7 +172,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)
}
Expand All @@ -138,7 +189,14 @@ func main() {
}()

env := envFromReader(rc)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the general comment, this reverses the overriding logic, so I think it is better to put it behind a CLI flag.

env = env.Merge(envFromEnv())
osEnv := envFromEnv()
if opts["<override_envs>"].(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]
Expand All @@ -147,6 +205,7 @@ func main() {
}
exit, ok := err.(*exec.ExitError)
if !ok {

log.Printf("non-exit error running %s: %v", name, err)
exitCode = 1
}
Expand Down