From 08265ed1d7833bf3bbc4d0f060f7e3277e6f1d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fouche=CC=81?= Date: Sat, 5 Jun 2021 15:54:10 -0300 Subject: [PATCH] Allow vars in dotenv paths, including environment variables Closes #453 Closes #434 Ref #433 Co-authored-by: Andrey Nering --- docs/usage.md | 11 ++++- internal/compiler/compiler.go | 1 + internal/compiler/v2/compiler_v2.go | 4 ++ internal/compiler/v3/compiler_v3.go | 19 ++++++-- task.go | 17 +++++++ task_test.go | 38 +++++++++++++++ taskfile/read/dotenv.go | 46 +++++++++++++++++++ taskfile/read/taskfile.go | 22 --------- testdata/dotenv/env_var_in_path/.env.testing | 1 + testdata/dotenv/env_var_in_path/Taskfile.yml | 8 ++++ .../dotenv/local_env_in_path/.env.testing | 1 + .../dotenv/local_env_in_path/Taskfile.yml | 11 +++++ .../dotenv/local_var_in_path/.env.testing | 1 + .../dotenv/local_var_in_path/Taskfile.yml | 13 ++++++ 14 files changed, 165 insertions(+), 28 deletions(-) create mode 100644 taskfile/read/dotenv.go create mode 100644 testdata/dotenv/env_var_in_path/.env.testing create mode 100644 testdata/dotenv/env_var_in_path/Taskfile.yml create mode 100644 testdata/dotenv/local_env_in_path/.env.testing create mode 100644 testdata/dotenv/local_env_in_path/Taskfile.yml create mode 100644 testdata/dotenv/local_var_in_path/.env.testing create mode 100644 testdata/dotenv/local_var_in_path/Taskfile.yml diff --git a/docs/usage.md b/docs/usage.md index 9e8d1a62..34628713 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -78,18 +78,25 @@ setting: KEYNAME=VALUE ``` +``` +# testing/.env +ENDPOINT=testing.com +``` ```yaml # Taskfile.yml version: '3' -dotenv: ['.env'] +env: + ENV: testing + +dotenv: ['.env', '{{.ENV}}/.env.', '{{.HOME}}/.env'] tasks: greet: cmds: - - echo "Using $KEYNAME" + - echo "Using $KEYNAME and endpoint $ENDPOINT" ``` ## Including other Taskfiles diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index b4305800..14ec1be7 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -7,6 +7,7 @@ import ( // Compiler handles compilation of a task before its execution. // E.g. variable merger, template processing, etc. type Compiler interface { + GetTaskfileVariables() (*taskfile.Vars, error) GetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error) FastGetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error) HandleDynamicVar(v taskfile.Var, dir string) (string, error) diff --git a/internal/compiler/v2/compiler_v2.go b/internal/compiler/v2/compiler_v2.go index d011a7ce..740b33b1 100644 --- a/internal/compiler/v2/compiler_v2.go +++ b/internal/compiler/v2/compiler_v2.go @@ -30,6 +30,10 @@ type CompilerV2 struct { muDynamicCache sync.Mutex } +func (c *CompilerV2) GetTaskfileVariables() (*taskfile.Vars, error) { + return &taskfile.Vars{}, nil +} + // FastGetVariables is a no-op on v2 func (c *CompilerV2) FastGetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error) { return c.GetVariables(t, call) diff --git a/internal/compiler/v3/compiler_v3.go b/internal/compiler/v3/compiler_v3.go index 41baab59..22b77b7c 100644 --- a/internal/compiler/v3/compiler_v3.go +++ b/internal/compiler/v3/compiler_v3.go @@ -29,17 +29,23 @@ type CompilerV3 struct { muDynamicCache sync.Mutex } +func (c *CompilerV3) GetTaskfileVariables() (*taskfile.Vars, error) { + return c.getVariables(nil, nil, true) +} + func (c *CompilerV3) GetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error) { - return c.getVariables(t, call, true) + return c.getVariables(t, &call, true) } func (c *CompilerV3) FastGetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error) { - return c.getVariables(t, call, false) + return c.getVariables(t, &call, false) } -func (c *CompilerV3) getVariables(t *taskfile.Task, call taskfile.Call, evaluateShVars bool) (*taskfile.Vars, error) { +func (c *CompilerV3) getVariables(t *taskfile.Task, call *taskfile.Call, evaluateShVars bool) (*taskfile.Vars, error) { result := compiler.GetEnviron() - result.Set("TASK", taskfile.Var{Static: t.Task}) + if t != nil { + result.Set("TASK", taskfile.Var{Static: t.Task}) + } getRangeFunc := func(dir string) func(k string, v taskfile.Var) error { return func(k string, v taskfile.Var) error { @@ -74,6 +80,11 @@ func (c *CompilerV3) getVariables(t *taskfile.Task, call taskfile.Call, evaluate if err := c.TaskfileVars.Range(rangeFunc); err != nil { return nil, err } + + if t == nil || call == nil { + return result, nil + } + if err := call.Vars.Range(rangeFunc); err != nil { return nil, err } diff --git a/task.go b/task.go index 4c268af1..d13694ad 100644 --- a/task.go +++ b/task.go @@ -176,6 +176,23 @@ func (e *Executor) Setup() error { } } + if v >= 3.0 { + env, err := read.Dotenv(e.Compiler, e.Taskfile, e.Dir) + if err != nil { + return err + } + + err = env.Range(func(key string, value taskfile.Var) error { + if _, ok := e.Taskfile.Env.Mapping[key]; !ok { + e.Taskfile.Env.Set(key, value) + } + return nil + }) + if err != nil { + return err + } + } + if v < 2.1 && e.Taskfile.Output != "" { return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`) } diff --git a/task_test.go b/task_test.go index 89b466ce..eedda82e 100644 --- a/task_test.go +++ b/task_test.go @@ -904,6 +904,44 @@ func TestDotenvShouldAllowMissingEnv(t *testing.T) { tt.Run(t) } +func TestDotenvHasLocalEnvInPath(t *testing.T) { + tt := fileContentTest{ + Dir: "testdata/dotenv/local_env_in_path", + Target: "default", + TrimSpace: false, + Files: map[string]string{ + "var.txt": "VAR='var_in_dot_env_1'\n", + }, + } + tt.Run(t) +} + +func TestDotenvHasLocalVarInPath(t *testing.T) { + tt := fileContentTest{ + Dir: "testdata/dotenv/local_var_in_path", + Target: "default", + TrimSpace: false, + Files: map[string]string{ + "var.txt": "VAR='var_in_dot_env_3'\n", + }, + } + tt.Run(t) +} + +func TestDotenvHasEnvVarInPath(t *testing.T) { + os.Setenv("ENV_VAR", "testing") + + tt := fileContentTest{ + Dir: "testdata/dotenv/env_var_in_path", + Target: "default", + TrimSpace: false, + Files: map[string]string{ + "var.txt": "VAR='var_in_dot_env_2'\n", + }, + } + tt.Run(t) +} + func TestExitImmediately(t *testing.T) { const dir = "testdata/exit_immediately" diff --git a/taskfile/read/dotenv.go b/taskfile/read/dotenv.go new file mode 100644 index 00000000..8d8ffd00 --- /dev/null +++ b/taskfile/read/dotenv.go @@ -0,0 +1,46 @@ +package read + +import ( + "os" + "path/filepath" + + "github.com/joho/godotenv" + + "github.com/go-task/task/v3/internal/compiler" + "github.com/go-task/task/v3/internal/templater" + "github.com/go-task/task/v3/taskfile" +) + +func Dotenv(c compiler.Compiler, tf *taskfile.Taskfile, dir string) (*taskfile.Vars, error) { + vars, err := c.GetTaskfileVariables() + if err != nil { + return nil, err + } + + env := &taskfile.Vars{} + + tr := templater.Templater{Vars: vars, RemoveNoValue: true} + + for _, dotEnvPath := range tf.Dotenv { + dotEnvPath = tr.Replace(dotEnvPath) + + if !filepath.IsAbs(dotEnvPath) { + dotEnvPath = filepath.Join(dir, dotEnvPath) + } + if _, err := os.Stat(dotEnvPath); os.IsNotExist(err) { + continue + } + + envs, err := godotenv.Read(dotEnvPath) + if err != nil { + return nil, err + } + for key, value := range envs { + if _, ok := env.Mapping[key]; !ok { + env.Set(key, taskfile.Var{Static: value}) + } + } + } + + return env, nil +} diff --git a/taskfile/read/taskfile.go b/taskfile/read/taskfile.go index e7fa6ba2..780edd8e 100644 --- a/taskfile/read/taskfile.go +++ b/taskfile/read/taskfile.go @@ -7,7 +7,6 @@ import ( "path/filepath" "runtime" - "github.com/joho/godotenv" "gopkg.in/yaml.v3" "github.com/go-task/task/v3/internal/templater" @@ -37,27 +36,6 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) { return nil, err } - if v >= 3.0 { - for _, dotEnvPath := range t.Dotenv { - if !filepath.IsAbs(dotEnvPath) { - dotEnvPath = filepath.Join(dir, dotEnvPath) - } - if _, err := os.Stat(dotEnvPath); os.IsNotExist(err) { - continue - } - - envs, err := godotenv.Read(dotEnvPath) - if err != nil { - return nil, err - } - for key, value := range envs { - if _, ok := t.Env.Mapping[key]; !ok { - t.Env.Set(key, taskfile.Var{Static: value}) - } - } - } - } - err = t.Includes.Range(func(namespace string, includedTask taskfile.IncludedTaskfile) error { if v >= 3.0 { tr := templater.Templater{Vars: &taskfile.Vars{}, RemoveNoValue: true} diff --git a/testdata/dotenv/env_var_in_path/.env.testing b/testdata/dotenv/env_var_in_path/.env.testing new file mode 100644 index 00000000..81bb94c3 --- /dev/null +++ b/testdata/dotenv/env_var_in_path/.env.testing @@ -0,0 +1 @@ +VAR_IN_DOTENV=var_in_dot_env_2 diff --git a/testdata/dotenv/env_var_in_path/Taskfile.yml b/testdata/dotenv/env_var_in_path/Taskfile.yml new file mode 100644 index 00000000..84c3a38e --- /dev/null +++ b/testdata/dotenv/env_var_in_path/Taskfile.yml @@ -0,0 +1,8 @@ +version: "3" + +dotenv: [".env.{{.ENV_VAR}}"] + +tasks: + default: + cmds: + - echo "VAR='$VAR_IN_DOTENV'" > var.txt diff --git a/testdata/dotenv/local_env_in_path/.env.testing b/testdata/dotenv/local_env_in_path/.env.testing new file mode 100644 index 00000000..b5004189 --- /dev/null +++ b/testdata/dotenv/local_env_in_path/.env.testing @@ -0,0 +1 @@ +VAR_IN_DOTENV=var_in_dot_env_1 diff --git a/testdata/dotenv/local_env_in_path/Taskfile.yml b/testdata/dotenv/local_env_in_path/Taskfile.yml new file mode 100644 index 00000000..27306777 --- /dev/null +++ b/testdata/dotenv/local_env_in_path/Taskfile.yml @@ -0,0 +1,11 @@ +version: "3" + +env: + LOCAL_ENV: testing + +dotenv: [".env.{{.LOCAL_ENV}}"] + +tasks: + default: + cmds: + - echo "VAR='$VAR_IN_DOTENV'" > var.txt diff --git a/testdata/dotenv/local_var_in_path/.env.testing b/testdata/dotenv/local_var_in_path/.env.testing new file mode 100644 index 00000000..95119f1c --- /dev/null +++ b/testdata/dotenv/local_var_in_path/.env.testing @@ -0,0 +1 @@ +VAR_IN_DOTENV=var_in_dot_env_3 diff --git a/testdata/dotenv/local_var_in_path/Taskfile.yml b/testdata/dotenv/local_var_in_path/Taskfile.yml new file mode 100644 index 00000000..c667d606 --- /dev/null +++ b/testdata/dotenv/local_var_in_path/Taskfile.yml @@ -0,0 +1,13 @@ +version: "3" + +vars: + PART_1: test + PART_2: ing + LOCAL_VAR: "{{.PART_1}}{{.PART_2}}" + +dotenv: [".env.{{.LOCAL_VAR}}"] + +tasks: + default: + cmds: + - echo "VAR='$VAR_IN_DOTENV'" > var.txt