From 8b3c34c308ccf2bc20c095637a2049f8f7f04a23 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Sun, 15 Apr 2018 11:11:07 -0300 Subject: [PATCH 1/5] Add "output" options to the Taskfile Also, fix handling of Taskfile by making the version an instance of `semver.Constraints` instead of `semver.Version`. This makes the version works as described on TASKFILE_VERSIONS.md document, i.e. version "2" will include "2.x" features but version "2.0" not. --- internal/taskfile/taskfile.go | 3 +++ internal/taskfile/version/version.go | 37 ++++++++++++---------------- task.go | 11 ++++++--- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/internal/taskfile/taskfile.go b/internal/taskfile/taskfile.go index b874c610..2ac0cf50 100644 --- a/internal/taskfile/taskfile.go +++ b/internal/taskfile/taskfile.go @@ -4,6 +4,7 @@ package taskfile type Taskfile struct { Version string Expansions int + Output string Vars Vars Tasks Tasks } @@ -18,6 +19,7 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error { var taskfile struct { Version string Expansions int + Output string Vars Vars Tasks Tasks } @@ -26,6 +28,7 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error { } tf.Version = taskfile.Version tf.Expansions = taskfile.Expansions + tf.Output = taskfile.Output tf.Vars = taskfile.Vars tf.Tasks = taskfile.Tasks if tf.Expansions <= 0 { diff --git a/internal/taskfile/version/version.go b/internal/taskfile/version/version.go index b0a6563b..2853c5c3 100644 --- a/internal/taskfile/version/version.go +++ b/internal/taskfile/version/version.go @@ -5,27 +5,30 @@ import ( ) var ( - v1 = mustVersion("1") - v2 = mustVersion("2") - - isV1 = mustConstraint("= 1") - isV2 = mustConstraint(">= 2") - isV21 = mustConstraint(">= 2.1") + v1 = mustVersion("1") + v2 = mustVersion("2") + v21 = mustVersion("2.1") + v22 = mustVersion("2.2") ) // IsV1 returns if is a given Taskfile version is version 1 -func IsV1(v *semver.Version) bool { - return isV1.Check(v) +func IsV1(v *semver.Constraints) bool { + return v.Check(v1) } // IsV2 returns if is a given Taskfile version is at least version 2 -func IsV2(v *semver.Version) bool { - return isV2.Check(v) +func IsV2(v *semver.Constraints) bool { + return v.Check(v2) } -// IsV21 returns if is a given Taskfile version is at least version 2 -func IsV21(v *semver.Version) bool { - return isV21.Check(v) +// IsV21 returns if is a given Taskfile version is at least version 2.1 +func IsV21(v *semver.Constraints) bool { + return v.Check(v21) +} + +// IsV22 returns if is a given Taskfile version is at least version 2.2 +func IsV22(v *semver.Constraints) bool { + return v.Check(v22) } func mustVersion(s string) *semver.Version { @@ -35,11 +38,3 @@ func mustVersion(s string) *semver.Version { } return v } - -func mustConstraint(s string) *semver.Constraints { - c, err := semver.NewConstraint(s) - if err != nil { - panic(err) - } - return c -} diff --git a/task.go b/task.go index cade5d35..60f7bb39 100644 --- a/task.go +++ b/task.go @@ -79,7 +79,7 @@ func (e *Executor) Setup() error { return err } - v, err := semver.NewVersion(e.Taskfile.Version) + v, err := semver.NewConstraint(e.Taskfile.Version) if err != nil { return fmt.Errorf(`task: could not parse taskfile version "%s": %v`, e.Taskfile.Version, err) } @@ -108,7 +108,7 @@ func (e *Executor) Setup() error { Vars: e.taskvars, Logger: e.Logger, } - case version.IsV2(v): + case version.IsV2(v), version.IsV21(v): e.Compiler = &compilerv2.CompilerV2{ Dir: e.Dir, Taskvars: e.taskvars, @@ -116,8 +116,11 @@ func (e *Executor) Setup() error { Expansions: e.Taskfile.Expansions, Logger: e.Logger, } - case version.IsV21(v): - return fmt.Errorf(`task: Taskfile versions greater than v2 not implemented in the version of Task`) + case version.IsV22(v): + return fmt.Errorf(`task: Taskfile versions greater than v2.1 not implemented in the version of Task`) + } + if !version.IsV21(v) && e.Taskfile.Output != "" { + return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`) } e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks)) From 051ff35878ab02ae5446b7c56f41ce06415295d8 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Sun, 15 Apr 2018 14:35:29 -0300 Subject: [PATCH 2/5] Add "output" option to Taskfile to control how stuff are printed to stdout/stderr First step for #104 --- internal/output/group.go | 26 ++++++++++++++++++++++++++ internal/output/interleaved.go | 23 +++++++++++++++++++++++ internal/output/output.go | 9 +++++++++ task.go | 20 ++++++++++++++++++-- 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 internal/output/group.go create mode 100644 internal/output/interleaved.go create mode 100644 internal/output/output.go diff --git a/internal/output/group.go b/internal/output/group.go new file mode 100644 index 00000000..1e5a434a --- /dev/null +++ b/internal/output/group.go @@ -0,0 +1,26 @@ +package output + +import ( + "bytes" + "io" +) + +type Group struct{} + +func (Group) WrapWriter(w io.Writer) io.WriteCloser { + return &groupWriter{writer: w} +} + +type groupWriter struct { + writer io.Writer + buff bytes.Buffer +} + +func (gw *groupWriter) Write(p []byte) (int, error) { + return gw.buff.Write(p) +} + +func (gw *groupWriter) Close() error { + _, err := io.Copy(gw.writer, &gw.buff) + return err +} diff --git a/internal/output/interleaved.go b/internal/output/interleaved.go new file mode 100644 index 00000000..9d37f485 --- /dev/null +++ b/internal/output/interleaved.go @@ -0,0 +1,23 @@ +package output + +import ( + "io" +) + +type Interleaved struct{} + +func (Interleaved) WrapWriter(w io.Writer) io.WriteCloser { + return nopWriterCloser{w: w} +} + +type nopWriterCloser struct { + w io.Writer +} + +func (wc nopWriterCloser) Write(p []byte) (int, error) { + return wc.w.Write(p) +} + +func (wc nopWriterCloser) Close() error { + return nil +} diff --git a/internal/output/output.go b/internal/output/output.go new file mode 100644 index 00000000..fd19bb4a --- /dev/null +++ b/internal/output/output.go @@ -0,0 +1,9 @@ +package output + +import ( + "io" +) + +type Output interface { + WrapWriter(io.Writer) io.WriteCloser +} diff --git a/task.go b/task.go index 60f7bb39..96f51bf6 100644 --- a/task.go +++ b/task.go @@ -12,6 +12,7 @@ import ( compilerv2 "github.com/go-task/task/internal/compiler/v2" "github.com/go-task/task/internal/execext" "github.com/go-task/task/internal/logger" + "github.com/go-task/task/internal/output" "github.com/go-task/task/internal/taskfile" "github.com/go-task/task/internal/taskfile/version" @@ -44,6 +45,7 @@ type Executor struct { Logger *logger.Logger Compiler compiler.Compiler + Output output.Output taskvars taskfile.Vars @@ -119,9 +121,18 @@ func (e *Executor) Setup() error { case version.IsV22(v): return fmt.Errorf(`task: Taskfile versions greater than v2.1 not implemented in the version of Task`) } + if !version.IsV21(v) && e.Taskfile.Output != "" { return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`) } + switch e.Taskfile.Output { + case "", "interleaved": + e.Output = output.Interleaved{} + case "group": + e.Output = output.Group{} + default: + return fmt.Errorf(`task: output option "%s" not recognized`, e.Taskfile.Output) + } e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks)) for k := range e.Taskfile.Tasks { @@ -193,14 +204,19 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi e.Logger.Errf(cmd.Cmd) } + stdOut := e.Output.WrapWriter(e.Stdout) + stdErr := e.Output.WrapWriter(e.Stderr) + defer stdOut.Close() + defer stdErr.Close() + return execext.RunCommand(&execext.RunCommandOptions{ Context: ctx, Command: cmd.Cmd, Dir: t.Dir, Env: getEnviron(t), Stdin: e.Stdin, - Stdout: e.Stdout, - Stderr: e.Stderr, + Stdout: stdOut, + Stderr: stdErr, }) } From a407b0a8eb56760ee225cc66dcf61002308f2de7 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Sun, 22 Apr 2018 15:41:53 -0300 Subject: [PATCH 3/5] First step implementing "prefixed" output option Ref #104 --- internal/output/group.go | 2 +- internal/output/interleaved.go | 2 +- internal/output/output.go | 2 +- internal/output/prefixed.go | 65 ++++++++++++++++++++++++++++++++++ internal/taskfile/task.go | 1 + task.go | 6 ++-- variables.go | 4 +++ 7 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 internal/output/prefixed.go diff --git a/internal/output/group.go b/internal/output/group.go index 1e5a434a..62259f34 100644 --- a/internal/output/group.go +++ b/internal/output/group.go @@ -7,7 +7,7 @@ import ( type Group struct{} -func (Group) WrapWriter(w io.Writer) io.WriteCloser { +func (Group) WrapWriter(w io.Writer, _ string) io.WriteCloser { return &groupWriter{writer: w} } diff --git a/internal/output/interleaved.go b/internal/output/interleaved.go index 9d37f485..305338d2 100644 --- a/internal/output/interleaved.go +++ b/internal/output/interleaved.go @@ -6,7 +6,7 @@ import ( type Interleaved struct{} -func (Interleaved) WrapWriter(w io.Writer) io.WriteCloser { +func (Interleaved) WrapWriter(w io.Writer, _ string) io.WriteCloser { return nopWriterCloser{w: w} } diff --git a/internal/output/output.go b/internal/output/output.go index fd19bb4a..f63b83a7 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -5,5 +5,5 @@ import ( ) type Output interface { - WrapWriter(io.Writer) io.WriteCloser + WrapWriter(w io.Writer, prefix string) io.WriteCloser } diff --git a/internal/output/prefixed.go b/internal/output/prefixed.go new file mode 100644 index 00000000..f9ea8578 --- /dev/null +++ b/internal/output/prefixed.go @@ -0,0 +1,65 @@ +package output + +import ( + "bytes" + "fmt" + "io" + "strings" +) + +type Prefixed struct{} + +func (Prefixed) WrapWriter(w io.Writer, prefix string) io.WriteCloser { + return &prefixWriter{writer: w, prefix: prefix} +} + +type prefixWriter struct { + writer io.Writer + prefix string + buff bytes.Buffer +} + +func (pw *prefixWriter) Write(p []byte) (int, error) { + n, err := pw.buff.Write(p) + if err != nil { + return n, err + } + + return n, pw.writeOutputLines(false) +} + +func (pw *prefixWriter) Close() error { + return pw.writeOutputLines(true) +} + +func (pw *prefixWriter) writeOutputLines(force bool) error { + for { + line, err := pw.buff.ReadString('\n') + if err == nil { + if err = pw.writeLine(line); err != nil { + return err + } + } else if err == io.EOF { + // if this line was not a complete line, re-add to the buffer + if !force && !strings.HasSuffix(line, "\n") { + _, err = pw.buff.WriteString(line) + return err + } + + return pw.writeLine(line) + } else { + return err + } + } +} + +func (pw *prefixWriter) writeLine(line string) error { + if line == "" { + return nil + } + if !strings.HasSuffix(line, "\n") { + line += "\n" + } + _, err := fmt.Fprintf(pw.writer, "[%s] %s", pw.prefix, line) + return err +} diff --git a/internal/taskfile/task.go b/internal/taskfile/task.go index 6a2b4708..cf15ea8a 100644 --- a/internal/taskfile/task.go +++ b/internal/taskfile/task.go @@ -17,4 +17,5 @@ type Task struct { Env Vars Silent bool Method string + Prefix string } diff --git a/task.go b/task.go index 96f51bf6..e2de4c62 100644 --- a/task.go +++ b/task.go @@ -130,6 +130,8 @@ func (e *Executor) Setup() error { e.Output = output.Interleaved{} case "group": e.Output = output.Group{} + case "prefixed": + e.Output = output.Prefixed{} default: return fmt.Errorf(`task: output option "%s" not recognized`, e.Taskfile.Output) } @@ -204,8 +206,8 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi e.Logger.Errf(cmd.Cmd) } - stdOut := e.Output.WrapWriter(e.Stdout) - stdErr := e.Output.WrapWriter(e.Stderr) + stdOut := e.Output.WrapWriter(e.Stdout, t.Prefix) + stdErr := e.Output.WrapWriter(e.Stderr, t.Prefix) defer stdOut.Close() defer stdErr.Close() diff --git a/variables.go b/variables.go index ca0722bb..e5d8b411 100644 --- a/variables.go +++ b/variables.go @@ -39,6 +39,7 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) { Env: r.ReplaceVars(origTask.Env), Silent: origTask.Silent, Method: r.Replace(origTask.Method), + Prefix: r.Replace(origTask.Prefix), } new.Dir, err = homedir.Expand(new.Dir) if err != nil { @@ -47,6 +48,9 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) { if e.Dir != "" && !filepath.IsAbs(new.Dir) { new.Dir = filepath.Join(e.Dir, new.Dir) } + if new.Prefix == "" { + new.Prefix = new.Task + } for k, v := range new.Env { static, err := e.Compiler.HandleDynamicVar(v) if err != nil { From 6be994f1cab4c339b93399d783f44b44d5f7ff0f Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Sun, 22 Apr 2018 21:40:03 -0300 Subject: [PATCH 4/5] Write test for ouput types --- Taskfile.yml | 1 + internal/output/output_test.go | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 internal/output/output_test.go diff --git a/Taskfile.yml b/Taskfile.yml index 8c2a2a7b..f92f726d 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -13,6 +13,7 @@ vars: ./internal/compiler/v2 ./internal/execext ./internal/logger + ./internal/output ./internal/status ./internal/taskfile ./internal/taskfile/version diff --git a/internal/output/output_test.go b/internal/output/output_test.go new file mode 100644 index 00000000..e2efd2db --- /dev/null +++ b/internal/output/output_test.go @@ -0,0 +1,62 @@ +package output_test + +import ( + "bytes" + "fmt" + "testing" + + "github.com/go-task/task/internal/output" + + "github.com/stretchr/testify/assert" +) + +func TestInterleaved(t *testing.T) { + var b bytes.Buffer + var o output.Output = output.Interleaved{} + var w = o.WrapWriter(&b, "") + + fmt.Fprintln(w, "foo\nbar") + assert.Equal(t, "foo\nbar\n", b.String()) + fmt.Fprintln(w, "baz") + assert.Equal(t, "foo\nbar\nbaz\n", b.String()) +} + +func TestGroup(t *testing.T) { + var b bytes.Buffer + var o output.Output = output.Group{} + var w = o.WrapWriter(&b, "") + + fmt.Fprintln(w, "foo\nbar") + assert.Equal(t, "", b.String()) + fmt.Fprintln(w, "baz") + assert.Equal(t, "", b.String()) + assert.NoError(t, w.Close()) + assert.Equal(t, "foo\nbar\nbaz\n", b.String()) +} + +func TestPrefixed(t *testing.T) { + var b bytes.Buffer + var o output.Output = output.Prefixed{} + var w = o.WrapWriter(&b, "prefix") + + t.Run("simple use cases", func(t *testing.T) { + b.Reset() + + fmt.Fprintln(w, "foo\nbar") + assert.Equal(t, "[prefix] foo\n[prefix] bar\n", b.String()) + fmt.Fprintln(w, "baz") + assert.Equal(t, "[prefix] foo\n[prefix] bar\n[prefix] baz\n", b.String()) + }) + + t.Run("multiple writes for a single line", func(t *testing.T) { + b.Reset() + + for _, char := range []string{"T", "e", "s", "t", "!"} { + fmt.Fprint(w, char) + assert.Equal(t, "", b.String()) + } + + assert.NoError(t, w.Close()) + assert.Equal(t, "[prefix] Test!\n", b.String()) + }) +} From cdbe821eb83eb455afc846b4c2a5f8183e113359 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Sun, 22 Apr 2018 22:13:31 -0300 Subject: [PATCH 5/5] Write documentation for output types --- README.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/README.md b/README.md index 502cd7a2..9595e4a5 100644 --- a/README.md +++ b/README.md @@ -632,6 +632,68 @@ tasks: - echo "This will print nothing" > /dev/null ``` +## Output syntax + +By default, Task just redirect the STDOUT and STDERR of the running commands +to the shell in real time. This is good for having live feedback for log +printed by commands, but the output can become messy if you have multiple +commands running at the same time and printing lots of stuff. + +To make this more customizable, there are currently three different output +options you can choose: + +- `interleaved` (default) +- `group` +- `prefixed` + + To choose another one, just set it to root in the Taskfile: + + ```yml +version: '2' + +output: 'group' + +tasks: + # ... + ``` + + The `group` output will print the entire output of a command once, after it + finishes, so you won't have live feedback for commands that take a long time + to run. + + The `prefix` output will prefix every line printed by a command with + `[task-name] ` as the prefix, but you can customize the prefix for a command + with the `prefix:` attribute: + + ```yml +version: '2' + +output: prefixed + +tasks: + default: + deps: + - task: print + vars: {TEXT: foo} + - task: print + vars: {TEXT: bar} + - task: print + vars: {TEXT: baz} + + print: + cmds: + - echo "{{.TEXT}}" + prefix: "print-{{.TEXT}}" + silent: true +``` + +```bash +$ task default +[print-foo] foo +[print-bar] bar +[print-baz] baz +``` + ## Watch tasks If you give a `--watch` or `-w` argument, task will watch for file changes