diff --git a/.gitignore b/.gitignore index bdf3441..59023bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .direnv bin/ +.secrets/ diff --git a/README.md b/README.md index 9a98a4e..cffc965 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ go install github.com/nxtcoder17/runfile/cmd/run@latest ``` ## Usage - + ### Runfile Create a `Runfile` in the root of your project, and add tasks to it. @@ -23,6 +23,10 @@ Create a `Runfile` in the root of your project, and add tasks to it. - [x] Run tasks - [x] Run tasks with Key-Value environment variables - [x] Run tasks with dynamic environment variables (by shell execution) +- [x] Run tasks with dotenv files as their environment variables +- [ ] Running tasks in different working directory [reference](https://taskfile.dev/reference/schema/#task) +- [ ] Running tasks with watch mode +- [ ] Running tasks in parallel ### Example @@ -30,9 +34,17 @@ Create a `Runfile` in the root of your project, and add tasks to it. version: 0.0.1 tasks: - build: - cmd: - - go build -o bin/run ./cmd/run test: - cmd: - - go test ./... + env: + key1: value1 + key2: value2 + key3: + sh: echo -n "hello" + dotenv: + - .secrets/env # load dotenv file + cmd: + - echo "value of key1 is '$key1'" + - echo "value of key2 is '$key2'" + - echo "value of key3 is '$key3'" + - echo "value of key4 is '$key4'" # assuming key4 is defined in .secrets/env +``` diff --git a/Runfile b/Runfile index 7eecea1..65116bd 100644 --- a/Runfile +++ b/Runfile @@ -4,9 +4,13 @@ version: 0.0.1 tasks: build: - # dir: ./cmd/run cmd: - |+ echo "building ..." - go build -o bin/run ./cmd/run + go build -o bin/run -ldflags="-s -w" -tags urfave_cli_no_docs cmd/run/main.go echo "DONE" + + example: + cmd: + - |+ + run -f ./examples/Runfile cook diff --git a/examples/Runfile b/examples/Runfile index dcd69e4..3130c74 100644 --- a/examples/Runfile +++ b/examples/Runfile @@ -1,5 +1,4 @@ # vim: set ft=yaml: - version: 0.0.1 tasks: @@ -8,17 +7,27 @@ tasks: k1: v1 k2: 'f"\( asfsadfssdfas asfd $Asdfasdfa' k3: - sh: echo "hello" + sh: echo -n "hello" + dotenv: + - .secrets/env cmd: - - echo "cook" - - "echo k1: $k1, k2: $k2, k3: $k3" + - echo "hi hello" + - echo "value of k1 is '$k1'" + - echo "value of k2 is '$k2'" + - echo "value of k3 is '$k3'" + - echo "value of key_id (from .dotenv) is '$key_id', ${#key_id}" + clean: name: clean shell: ["python", "-c"] + dotenv: + - .secrets/env cmd: - |+ import secrets - print(secrets.token_hex(32)) + import os + print(os.environ['key_id']) + # print(secrets.token_hex(32)) laundry: name: laundry shell: ["node", "-e"] diff --git a/pkg/runfile/parser.go b/pkg/runfile/parser.go index 4149e80..33ae39b 100644 --- a/pkg/runfile/parser.go +++ b/pkg/runfile/parser.go @@ -1,7 +1,11 @@ package runfile import ( + "bufio" + "fmt" "os" + "strconv" + "strings" "sigs.k8s.io/yaml" ) @@ -18,3 +22,35 @@ func ParseRunFile(file string) (*RunFile, error) { } return &runfile, nil } + +// parseDotEnv parses the .env file and returns a slice of strings as in os.Environ() +func parseDotEnv(files ...string) ([]string, error) { + results := make([]string, 0, 5) + + for i := range files { + f, err := os.Open(files[i]) + if err != nil { + return nil, err + } + + s := bufio.NewScanner(f) + for s.Scan() { + s2 := strings.SplitN(s.Text(), "=", 2) + if len(s2) != 2 { + continue + } + s, _ := strconv.Unquote(string(s2[1])) + + // os.Setenv(s2[0], s2[1]) + os.Setenv(s2[0], s) + results = append(results, s2[0]) + } + } + + for i := range results { + v := os.Getenv(results[i]) + results[i] = fmt.Sprintf("%s=%v", results[i], v) + } + + return results, nil +} diff --git a/pkg/runfile/run.go b/pkg/runfile/run.go index be3d87f..21b70ef 100644 --- a/pkg/runfile/run.go +++ b/pkg/runfile/run.go @@ -11,7 +11,7 @@ import ( type runArgs struct { shell []string - env map[string]string + env []string // [key=value, key=value, ...] cmd string stdout io.Writer @@ -39,15 +39,10 @@ func runInShell(ctx context.Context, args runArgs) error { // f.WriteString(args.cmd) // f.Close() - environ := os.Environ() - for k, v := range args.env { - environ = append(environ, fmt.Sprintf("%s=%v", k, v)) - } - // cargs := append(args.shell[1:], f.Name()) cargs := append(args.shell[1:], args.cmd) c := exec.CommandContext(ctx, shell, cargs...) - c.Env = environ + c.Env = args.env c.Stdout = args.stdout c.Stderr = args.stderr return c.Run() @@ -59,11 +54,11 @@ func (r *RunFile) Run(ctx context.Context, taskName string) error { return fmt.Errorf("task %s not found", taskName) } - env := make(map[string]string, len(task.Env)) + env := make([]string, len(task.Env)) for k, v := range task.Env { switch v := v.(type) { case string: - env[k] = v + env = append(env, fmt.Sprintf("%s=%s", k, v)) case map[string]any: shcmd, ok := v["sh"] if !ok { @@ -79,22 +74,31 @@ func (r *RunFile) Run(ctx context.Context, taskName string) error { if err := runInShell(ctx, runArgs{ shell: task.Shell, - env: env, + env: os.Environ(), cmd: s, stdout: value, }); err != nil { return err } - env[k] = value.String() + env = append(env, fmt.Sprintf("%s=%v", k, value.String())) default: panic(fmt.Sprintf("env %s is not a string (%T)", k, v)) } } + // parsing dotenv + s, err := parseDotEnv(task.DotEnv...) + if err != nil { + return err + } + + // INFO: keys from task.Env will override those coming from dotenv files, when duplicated + env = append(s, env...) + for _, cmd := range task.Commands { runInShell(ctx, runArgs{ shell: task.Shell, - env: env, + env: append(os.Environ(), env...), cmd: cmd, }) } diff --git a/pkg/runfile/type.go b/pkg/runfile/type.go index 1e90b37..f62304d 100644 --- a/pkg/runfile/type.go +++ b/pkg/runfile/type.go @@ -7,6 +7,8 @@ type RunFile struct { } type TaskSpec struct { + // load env vars from [.env](https://www.google.com/search?q=sample+dotenv+files&udm=2) files + DotEnv []string `json:"dotenv"` Env map[string]any `json:"env"` Commands []string `json:"cmd"` Shell []string `json:"shell"`