From 95997fd51960a44519bf9467903bacabc2b46028 Mon Sep 17 00:00:00 2001 From: zakuro Date: Sat, 24 Oct 2020 14:52:04 +0900 Subject: [PATCH] First commit --- README.md | 13 +++++ cli.go | 132 ++++++++++++++++++++++++++++++++++++++++++++ config.go | 32 +++++++++++ config_test.go | 58 +++++++++++++++++++ examples/cc.yaml | 12 ++++ examples/hello.yaml | 18 ++++++ examples_test.go | 18 ++++++ go.mod | 8 +++ go.sum | 34 ++++++++++++ main.go | 9 +++ task.go | 44 +++++++++++++++ 11 files changed, 378 insertions(+) create mode 100644 README.md create mode 100644 cli.go create mode 100644 config.go create mode 100644 config_test.go create mode 100644 examples/cc.yaml create mode 100644 examples/hello.yaml create mode 100644 examples_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 task.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..c3a0f5f --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Z + +Simple task runner + +# Installation + +``` +go get github.com/zakuro9715/z +``` + +# Examples + +See [Examples](./examples) and [Examples Test](./examples_test.go) diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..d3206b6 --- /dev/null +++ b/cli.go @@ -0,0 +1,132 @@ +package main + +import ( + "fmt" + "io" + "os" + "strings" +) + +var exit = os.Exit + +const version = "v0.1.0" +const helpTextBase = ` +z - Z Task runner + +Usage: + z [OPTIONS] task... [ARGS] +` + +func isHelpFlag(s string) bool { + return s == "-h" || s == "--help" +} + +func showTextAndExit(code int, text string) { + if code == 0 { + fmt.Println(text) + } else { + fmt.Fprintln(os.Stderr, text) + } + exit(code) +} + +func fprintTasks(w io.Writer, tasks map[string]*Task) { + fmt.Fprintln(w, "Tasks:") + maxNameLen := 0 + for k := range tasks { + if len(k) > maxNameLen { + maxNameLen = len(k) + } + } + for k, t := range tasks { + fmt.Fprintf(w, + " %v%v - %v\n", + k, + strings.Repeat(" ", maxNameLen-len(k)), + t.Description, + ) + } +} + +func fprintHelp(w io.Writer, config *Config) { + var sb strings.Builder + sb.WriteString(helpTextBase) + if config != nil { + sb.WriteString("\n") + fprintTasks(&sb, config.Tasks) + } + fmt.Fprintln(w, sb.String()) +} + +func fprintTaskHelp(w io.Writer, task *Task) { + var sb strings.Builder + sb.WriteString(task.Description) + if len(task.Tasks) > 0 { + fprintTasks(&sb, task.Tasks) + } + fmt.Fprint(w, sb.String()) +} + +func fprintVersion(w io.Writer) { + fmt.Fprintf(w, "z task runner %v\n", version) +} + +func realMain(args []string) int { + i := 0 + configPath := "z.yaml" + if p, ok := os.LookupEnv("ZCONFIG"); ok { + configPath = p + } + + config, err := LoadConfig(configPath) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return 1 + } + + for ; i < len(args); i++ { + arg := args[i] + if arg[0] != '-' { + break + } + switch { + case isHelpFlag(arg): + fprintHelp(os.Stdout, config) + return 0 + case arg == "-v" || arg == "--version": + fprintVersion(os.Stdout) + default: // unknow flag + fprintHelp(os.Stderr, nil) + return 1 + } + } + + if i >= len(args) { + fprintHelp(os.Stderr, config) + return 1 + } + + task, ok := config.Tasks[args[i]] + if !ok { + fmt.Fprintf(os.Stderr, "Unknown task: %v\n", args[i]) + exit(1) + } + i++ + for ; i < len(args); i++ { + if isHelpFlag(args[i]) { + fprintTaskHelp(os.Stdout, task) + return 0 + } + if args[i] == "--" { + i += 1 + break + } + if subtask, ok := task.Tasks[args[i]]; ok { + task = subtask + } else { + break + } + } + task.Run(args[i:]) + return 0 +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..22287c4 --- /dev/null +++ b/config.go @@ -0,0 +1,32 @@ +package main + +import ( + "io/ioutil" + + "github.com/goccy/go-yaml" +) + +type Config struct { + Shell string `yaml:"shell"` + Tasks Tasks `yaml:"tasks"` +} + +func LoadConfig(filename string) (*Config, error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + config := new(Config) + if err = yaml.Unmarshal(data, config); err != nil { + return nil, err + } + for _, t := range config.Tasks { + t.setConfig(config) + } + return config, nil +} + +type Hooks struct { + Pre string `yaml:"pre"` + Post string `yaml:"post"` +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..2e6032b --- /dev/null +++ b/config_test.go @@ -0,0 +1,58 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExampleIsValid(t *testing.T) { + assert.NoError(t, + filepath.Walk("examples", func(path string, info os.FileInfo, err error) error { + assert.NoError(t, err) + if info.IsDir() { + return nil + } + _, err = LoadConfig(path) + return err + }), + ) +} + +func TestLoadHelloExample(t *testing.T) { + expected := &Config{ + Shell: "bash", + Tasks: map[string]*Task{ + "hello": { + Cmds: []string{"echo hello"}, + Hooks: Hooks{ + Pre: "echo saying hello", + Post: "echo said hello", + }, + Tasks: map[string]*Task{ + "world": { + Cmds: []string{"z hello -- world"}, + Description: "Hello World", + }, + "cjk": { + Cmds: []string{ + "z hello japan", + "z hello korea", + "z hello china", + }, + }, + }, + }, + }, + } + for _, task := range expected.Tasks { + task.setConfig(expected) + } + + actual, err := LoadConfig("examples/hello.yaml") + assert.NoError(t, err) + + assert.Equal(t, expected, actual) +} diff --git a/examples/cc.yaml b/examples/cc.yaml new file mode 100644 index 0000000..1000ec4 --- /dev/null +++ b/examples/cc.yaml @@ -0,0 +1,12 @@ +tasks: + compile: + run: + - clang $@ + desc: Compile + hooks: + pre: echo Compiling + post: echo Compiled + tasks: + main: + run: + - z -c examples/cc.yaml compile main.c diff --git a/examples/hello.yaml b/examples/hello.yaml new file mode 100644 index 0000000..4ccb439 --- /dev/null +++ b/examples/hello.yaml @@ -0,0 +1,18 @@ +shell: bash +tasks: + hello: + run: + - echo hello + hooks: + pre: echo saying hello + post: echo said hello + tasks: + world: + desc: Hello World + run: + - z hello -- world + cjk: + run: + - z hello japan + - z hello korea + - z hello china diff --git a/examples_test.go b/examples_test.go new file mode 100644 index 0000000..f2c332b --- /dev/null +++ b/examples_test.go @@ -0,0 +1,18 @@ +package main + +import ( + "os" +) + +func ExampleHello() { + os.Setenv("ZCONFIG", "examples/hello.yaml") + realMain([]string{"hello"}) + realMain([]string{"hello", "world"}) + realMain([]string{"hello", "cjk"}) + // Output: + // hello + // hello world + // hello japan + // hello korea + // hello china +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..66ff366 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/zakuro9715/z + +go 1.15 + +require ( + github.com/goccy/go-yaml v1.8.3 + github.com/stretchr/testify v1.6.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..410a38b --- /dev/null +++ b/go.sum @@ -0,0 +1,34 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/goccy/go-yaml v1.8.3 h1:VGzw2KWSUyQX0yXai02S0nttBc+Oa4Kvh6RCFoxt8SE= +github.com/goccy/go-yaml v1.8.3/go.mod h1:wS4gNoLalDSJxo/SpngzPQ2BN4uuZVLCmbM4S3vd4+Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.30.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..c43c607 --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "os" +) + +func main() { + os.Exit(realMain(os.Args[1:])) +} diff --git a/task.go b/task.go new file mode 100644 index 0000000..d7d88c5 --- /dev/null +++ b/task.go @@ -0,0 +1,44 @@ +package main + +import ( + "os" + "os/exec" + "strings" +) + +type Tasks map[string]*Task +type Task struct { + Cmds []string `yaml:"run"` + Config *Config + Description string `yaml:"desc"` + Hooks Hooks `yaml:"hooks"` + Tasks Tasks `yaml:"tasks"` +} + +func (t *Task) setConfig(c *Config) { + t.Config = c + for _, sub := range t.Tasks { + sub.setConfig(c) + } +} + +func (t *Task) Run(args []string) error { + shell := t.Config.GetShell() + for _, command := range t.Cmds { + cmd := exec.Command(shell, "-c", command+" "+strings.Join(args, " ")) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return err + } + } + return nil +} + +func (c *Config) GetShell() string { + if len(c.Shell) == 0 { + return "sh" + } + return c.Shell +}