diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..a5dbbcb --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake . diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bdf3441 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.direnv +bin/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a98a4e --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +## Runfile is a super simple task runner + +All it does is run some pre-configured tasks for you, like running your applications, tests, building binaries, or some other scripts. + +It is inspired by other task runners like Taskfile, Make etc. + +But source code of those tools are like super big, and complex. So I decided to make a simpler one. + +## Installation + +```bash +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. + +### Features + +- [x] Run tasks +- [x] Run tasks with Key-Value environment variables +- [x] Run tasks with dynamic environment variables (by shell execution) + +### Example + +```yaml +version: 0.0.1 + +tasks: + build: + cmd: + - go build -o bin/run ./cmd/run + test: + cmd: + - go test ./... diff --git a/Runfile b/Runfile new file mode 100644 index 0000000..7eecea1 --- /dev/null +++ b/Runfile @@ -0,0 +1,12 @@ +# vim: set ft=yaml: + +version: 0.0.1 + +tasks: + build: + # dir: ./cmd/run + cmd: + - |+ + echo "building ..." + go build -o bin/run ./cmd/run + echo "DONE" diff --git a/cmd/run/completions/fish/run.fish b/cmd/run/completions/fish/run.fish new file mode 100644 index 0000000..d9aa4af --- /dev/null +++ b/cmd/run/completions/fish/run.fish @@ -0,0 +1,25 @@ +# run fish shell completion +set PROGNAME run + +function __fetch_runnable_tasks --description 'fetches all runnable tasks' + for i in (commandline -opc) + if contains -- $i help h + return 1 + end + end + + # Grab names and descriptions (if any) of the tasks + set -l output (run --generate-shell-completion | string split0) + echo "$output" > /tmp/test.txt + if test $output + echo $output + end + + return 0 +end + +complete -c run -d "runs a task with given name" -xa "(__fish_run_no_subcommand)" +complete -c run -n '__fish_run_no_subcommand' -f -l help -s h -d 'show help' +complete -c run -n '__fish_run_no_subcommand' -f -l help -s h -d 'show help' +complete -r -c run -n '__fish_run_no_subcommand' -a 'help h' -d 'Shows a list of commands or help for one command' + diff --git a/cmd/run/main.go b/cmd/run/main.go new file mode 100644 index 0000000..bfe2a49 --- /dev/null +++ b/cmd/run/main.go @@ -0,0 +1,110 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "path/filepath" + "syscall" + + "github.com/nxtcoder17/runfile/pkg/runfile" + "github.com/urfave/cli/v3" +) + +var Version string = "0.0.1" + +func main() { + cmd := cli.Command{ + Name: "run", + Version: Version, + Description: "A simple task runner", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Value: "", + }, + }, + EnableShellCompletion: true, + ShellComplete: func(ctx context.Context, c *cli.Command) { + if c.NArg() > 0 { + return + } + + runfilePath, err := locateRunfile(c) + if err != nil { + panic(err) + } + + runfile, err := runfile.ParseRunFile(runfilePath) + if err != nil { + panic(err) + } + + for k := range runfile.Tasks { + fmt.Fprintf(c.Root().Writer, "%s\n", k) + } + }, + Action: func(ctx context.Context, c *cli.Command) error { + if c.Args().Len() > 1 { + return fmt.Errorf("too many arguments") + } + if c.Args().Len() != 1 { + return fmt.Errorf("missing argument") + } + + runfilePath, err := locateRunfile(c) + if err != nil { + return err + } + + runfile, err := runfile.ParseRunFile(runfilePath) + if err != nil { + panic(err) + } + + s := c.Args().First() + return runfile.Run(ctx, s) + }, + } + + ctx, cf := context.WithCancel(context.TODO()) + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + fmt.Println("\n\rcanceling...") + cf() + os.Exit(1) + }() + + if err := cmd.Run(ctx, os.Args); err != nil { + log.Fatal(err) + } +} + +func locateRunfile(c *cli.Command) (string, error) { + var runfilePath string + switch { + case c.IsSet("file"): + runfilePath = c.String("file") + default: + dir, err := os.Getwd() + if err != nil { + return "", err + } + for { + _, err := os.Stat(filepath.Join(dir, "Runfile")) + if err != nil { + dir = filepath.Dir(dir) + continue + } + runfilePath = filepath.Join(dir, "Runfile") + break + } + } + return runfilePath, nil +} diff --git a/examples/Runfile b/examples/Runfile new file mode 100644 index 0000000..dcd69e4 --- /dev/null +++ b/examples/Runfile @@ -0,0 +1,38 @@ +# vim: set ft=yaml: + +version: 0.0.1 + +tasks: + cook: + env: + k1: v1 + k2: 'f"\( asfsadfssdfas asfd $Asdfasdfa' + k3: + sh: echo "hello" + cmd: + - echo "cook" + - "echo k1: $k1, k2: $k2, k3: $k3" + clean: + name: clean + shell: ["python", "-c"] + cmd: + - |+ + import secrets + print(secrets.token_hex(32)) + laundry: + name: laundry + shell: ["node", "-e"] + cmd: + - console.log("laundry") + eat: + name: eat + cmd: + - echo "eat" + sleep: + name: sleep + cmd: + - echo "sleep" + code: + name: code + cmd: + - echo "code" diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..3a3e95c --- /dev/null +++ b/flake.lock @@ -0,0 +1,60 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "id": "flake-utils", + "type": "indirect" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1726062873, + "narHash": "sha256-IiA3jfbR7K/B5+9byVi9BZGWTD4VSbWe8VLpp9B/iYk=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "4f807e8940284ad7925ebd0a0993d2a1791acb2f", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..dad0011 --- /dev/null +++ b/flake.nix @@ -0,0 +1,38 @@ +{ + description = "RunFile dev workspace"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + devShells.default = pkgs.mkShell { + # hardeningDisable = [ "all" ]; + + buildInputs = with pkgs; [ + # cli tools + curl + jq + yq + + pre-commit + + # programming tools + go_1_22 + + upx + ]; + + shellHook = '' + ''; + }; + } + ); +} + + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e4160a9 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/nxtcoder17/runfile + +go 1.22.7 + +require ( + github.com/urfave/cli/v3 v3.0.0-alpha9 + sigs.k8s.io/yaml v1.4.0 +) + +require github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8c4c5ae --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo= +github.com/urfave/cli/v3 v3.0.0-alpha9/go.mod h1:0kK/RUFHyh+yIKSfWxwheGndfnrvYSmYFVeKCh03ZUc= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/main.go b/main.go new file mode 100644 index 0000000..57040c6 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("RUN FILE") +} diff --git a/pkg/runfile/parser.go b/pkg/runfile/parser.go new file mode 100644 index 0000000..4149e80 --- /dev/null +++ b/pkg/runfile/parser.go @@ -0,0 +1,20 @@ +package runfile + +import ( + "os" + + "sigs.k8s.io/yaml" +) + +func ParseRunFile(file string) (*RunFile, error) { + var runfile RunFile + f, err := os.ReadFile(file) + if err != nil { + return &runfile, err + } + err = yaml.Unmarshal(f, &runfile) + if err != nil { + return &runfile, err + } + return &runfile, nil +} diff --git a/pkg/runfile/run.go b/pkg/runfile/run.go new file mode 100644 index 0000000..be3d87f --- /dev/null +++ b/pkg/runfile/run.go @@ -0,0 +1,102 @@ +package runfile + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "os/exec" +) + +type runArgs struct { + shell []string + env map[string]string + cmd string + + stdout io.Writer + stderr io.Writer +} + +func runInShell(ctx context.Context, args runArgs) error { + if args.shell == nil { + args.shell = []string{"sh", "-c"} + } + + if args.stdout == nil { + args.stdout = os.Stdout + } + + if args.stderr == nil { + args.stderr = os.Stderr + } + + shell := args.shell[0] + // f, err := os.CreateTemp(os.TempDir(), "runfile-") + // if err != nil { + // return err + // } + // 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.Stdout = args.stdout + c.Stderr = args.stderr + return c.Run() +} + +func (r *RunFile) Run(ctx context.Context, taskName string) error { + task, ok := r.Tasks[taskName] + if !ok { + return fmt.Errorf("task %s not found", taskName) + } + + env := make(map[string]string, len(task.Env)) + for k, v := range task.Env { + switch v := v.(type) { + case string: + env[k] = v + case map[string]any: + shcmd, ok := v["sh"] + if !ok { + return fmt.Errorf("env %s is not a string", k) + } + + s, ok := shcmd.(string) + if !ok { + return fmt.Errorf("shell cmd is not a string") + } + + value := new(bytes.Buffer) + + if err := runInShell(ctx, runArgs{ + shell: task.Shell, + env: env, + cmd: s, + stdout: value, + }); err != nil { + return err + } + env[k] = value.String() + default: + panic(fmt.Sprintf("env %s is not a string (%T)", k, v)) + } + } + + for _, cmd := range task.Commands { + runInShell(ctx, runArgs{ + shell: task.Shell, + env: env, + cmd: cmd, + }) + } + return nil +} diff --git a/pkg/runfile/type.go b/pkg/runfile/type.go new file mode 100644 index 0000000..1e90b37 --- /dev/null +++ b/pkg/runfile/type.go @@ -0,0 +1,13 @@ +package runfile + +type RunFile struct { + Version string + + Tasks map[string]TaskSpec `json:"tasks"` +} + +type TaskSpec struct { + Env map[string]any `json:"env"` + Commands []string `json:"cmd"` + Shell []string `json:"shell"` +}