1
0
mirror of https://github.com/go-task/task.git synced 2025-07-15 01:35:00 +02:00

Merge pull request #109 from go-task/feature/parallel-task-output-#104

Add "output" option to Taskfile to control how stuff are printed to stdout/stderr
This commit is contained in:
Andrey Nering
2018-04-28 15:18:05 -03:00
committed by GitHub
12 changed files with 299 additions and 27 deletions

View File

@ -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

View File

@ -13,6 +13,7 @@ vars:
./internal/compiler/v2
./internal/execext
./internal/logger
./internal/output
./internal/status
./internal/taskfile
./internal/taskfile/version

26
internal/output/group.go Normal file
View File

@ -0,0 +1,26 @@
package output
import (
"bytes"
"io"
)
type Group struct{}
func (Group) WrapWriter(w io.Writer, _ string) 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
}

View File

@ -0,0 +1,23 @@
package output
import (
"io"
)
type Interleaved struct{}
func (Interleaved) WrapWriter(w io.Writer, _ string) 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
}

View File

@ -0,0 +1,9 @@
package output
import (
"io"
)
type Output interface {
WrapWriter(w io.Writer, prefix string) io.WriteCloser
}

View File

@ -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())
})
}

View File

@ -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
}

View File

@ -17,4 +17,5 @@ type Task struct {
Env Vars
Silent bool
Method string
Prefix string
}

View File

@ -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 {

View File

@ -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
}

33
task.go
View File

@ -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
@ -79,7 +81,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 +110,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 +118,22 @@ 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`)
}
switch e.Taskfile.Output {
case "", "interleaved":
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)
}
e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks))
@ -190,14 +206,19 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
e.Logger.Errf(cmd.Cmd)
}
stdOut := e.Output.WrapWriter(e.Stdout, t.Prefix)
stdErr := e.Output.WrapWriter(e.Stderr, t.Prefix)
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,
})
}

View File

@ -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 {