mirror of
https://github.com/go-task/task.git
synced 2025-07-17 01:43:07 +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:
62
README.md
62
README.md
@ -632,6 +632,68 @@ tasks:
|
|||||||
- echo "This will print nothing" > /dev/null
|
- 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
|
## Watch tasks
|
||||||
|
|
||||||
If you give a `--watch` or `-w` argument, task will watch for file changes
|
If you give a `--watch` or `-w` argument, task will watch for file changes
|
||||||
|
@ -13,6 +13,7 @@ vars:
|
|||||||
./internal/compiler/v2
|
./internal/compiler/v2
|
||||||
./internal/execext
|
./internal/execext
|
||||||
./internal/logger
|
./internal/logger
|
||||||
|
./internal/output
|
||||||
./internal/status
|
./internal/status
|
||||||
./internal/taskfile
|
./internal/taskfile
|
||||||
./internal/taskfile/version
|
./internal/taskfile/version
|
||||||
|
26
internal/output/group.go
Normal file
26
internal/output/group.go
Normal 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
|
||||||
|
}
|
23
internal/output/interleaved.go
Normal file
23
internal/output/interleaved.go
Normal 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
|
||||||
|
}
|
9
internal/output/output.go
Normal file
9
internal/output/output.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package output
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Output interface {
|
||||||
|
WrapWriter(w io.Writer, prefix string) io.WriteCloser
|
||||||
|
}
|
62
internal/output/output_test.go
Normal file
62
internal/output/output_test.go
Normal 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())
|
||||||
|
})
|
||||||
|
}
|
65
internal/output/prefixed.go
Normal file
65
internal/output/prefixed.go
Normal 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
|
||||||
|
}
|
@ -17,4 +17,5 @@ type Task struct {
|
|||||||
Env Vars
|
Env Vars
|
||||||
Silent bool
|
Silent bool
|
||||||
Method string
|
Method string
|
||||||
|
Prefix string
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ package taskfile
|
|||||||
type Taskfile struct {
|
type Taskfile struct {
|
||||||
Version string
|
Version string
|
||||||
Expansions int
|
Expansions int
|
||||||
|
Output string
|
||||||
Vars Vars
|
Vars Vars
|
||||||
Tasks Tasks
|
Tasks Tasks
|
||||||
}
|
}
|
||||||
@ -18,6 +19,7 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||||||
var taskfile struct {
|
var taskfile struct {
|
||||||
Version string
|
Version string
|
||||||
Expansions int
|
Expansions int
|
||||||
|
Output string
|
||||||
Vars Vars
|
Vars Vars
|
||||||
Tasks Tasks
|
Tasks Tasks
|
||||||
}
|
}
|
||||||
@ -26,6 +28,7 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||||||
}
|
}
|
||||||
tf.Version = taskfile.Version
|
tf.Version = taskfile.Version
|
||||||
tf.Expansions = taskfile.Expansions
|
tf.Expansions = taskfile.Expansions
|
||||||
|
tf.Output = taskfile.Output
|
||||||
tf.Vars = taskfile.Vars
|
tf.Vars = taskfile.Vars
|
||||||
tf.Tasks = taskfile.Tasks
|
tf.Tasks = taskfile.Tasks
|
||||||
if tf.Expansions <= 0 {
|
if tf.Expansions <= 0 {
|
||||||
|
@ -5,27 +5,30 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
v1 = mustVersion("1")
|
v1 = mustVersion("1")
|
||||||
v2 = mustVersion("2")
|
v2 = mustVersion("2")
|
||||||
|
v21 = mustVersion("2.1")
|
||||||
isV1 = mustConstraint("= 1")
|
v22 = mustVersion("2.2")
|
||||||
isV2 = mustConstraint(">= 2")
|
|
||||||
isV21 = mustConstraint(">= 2.1")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsV1 returns if is a given Taskfile version is version 1
|
// IsV1 returns if is a given Taskfile version is version 1
|
||||||
func IsV1(v *semver.Version) bool {
|
func IsV1(v *semver.Constraints) bool {
|
||||||
return isV1.Check(v)
|
return v.Check(v1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsV2 returns if is a given Taskfile version is at least version 2
|
// IsV2 returns if is a given Taskfile version is at least version 2
|
||||||
func IsV2(v *semver.Version) bool {
|
func IsV2(v *semver.Constraints) bool {
|
||||||
return isV2.Check(v)
|
return v.Check(v2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsV21 returns if is a given Taskfile version is at least version 2
|
// IsV21 returns if is a given Taskfile version is at least version 2.1
|
||||||
func IsV21(v *semver.Version) bool {
|
func IsV21(v *semver.Constraints) bool {
|
||||||
return isV21.Check(v)
|
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 {
|
func mustVersion(s string) *semver.Version {
|
||||||
@ -35,11 +38,3 @@ func mustVersion(s string) *semver.Version {
|
|||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustConstraint(s string) *semver.Constraints {
|
|
||||||
c, err := semver.NewConstraint(s)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
33
task.go
33
task.go
@ -12,6 +12,7 @@ import (
|
|||||||
compilerv2 "github.com/go-task/task/internal/compiler/v2"
|
compilerv2 "github.com/go-task/task/internal/compiler/v2"
|
||||||
"github.com/go-task/task/internal/execext"
|
"github.com/go-task/task/internal/execext"
|
||||||
"github.com/go-task/task/internal/logger"
|
"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"
|
||||||
"github.com/go-task/task/internal/taskfile/version"
|
"github.com/go-task/task/internal/taskfile/version"
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ type Executor struct {
|
|||||||
|
|
||||||
Logger *logger.Logger
|
Logger *logger.Logger
|
||||||
Compiler compiler.Compiler
|
Compiler compiler.Compiler
|
||||||
|
Output output.Output
|
||||||
|
|
||||||
taskvars taskfile.Vars
|
taskvars taskfile.Vars
|
||||||
|
|
||||||
@ -79,7 +81,7 @@ func (e *Executor) Setup() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := semver.NewVersion(e.Taskfile.Version)
|
v, err := semver.NewConstraint(e.Taskfile.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(`task: could not parse taskfile version "%s": %v`, e.Taskfile.Version, err)
|
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,
|
Vars: e.taskvars,
|
||||||
Logger: e.Logger,
|
Logger: e.Logger,
|
||||||
}
|
}
|
||||||
case version.IsV2(v):
|
case version.IsV2(v), version.IsV21(v):
|
||||||
e.Compiler = &compilerv2.CompilerV2{
|
e.Compiler = &compilerv2.CompilerV2{
|
||||||
Dir: e.Dir,
|
Dir: e.Dir,
|
||||||
Taskvars: e.taskvars,
|
Taskvars: e.taskvars,
|
||||||
@ -116,8 +118,22 @@ func (e *Executor) Setup() error {
|
|||||||
Expansions: e.Taskfile.Expansions,
|
Expansions: e.Taskfile.Expansions,
|
||||||
Logger: e.Logger,
|
Logger: e.Logger,
|
||||||
}
|
}
|
||||||
case version.IsV21(v):
|
case version.IsV22(v):
|
||||||
return fmt.Errorf(`task: Taskfile versions greater than v2 not implemented in the version of Task`)
|
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))
|
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)
|
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{
|
return execext.RunCommand(&execext.RunCommandOptions{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Command: cmd.Cmd,
|
Command: cmd.Cmd,
|
||||||
Dir: t.Dir,
|
Dir: t.Dir,
|
||||||
Env: getEnviron(t),
|
Env: getEnviron(t),
|
||||||
Stdin: e.Stdin,
|
Stdin: e.Stdin,
|
||||||
Stdout: e.Stdout,
|
Stdout: stdOut,
|
||||||
Stderr: e.Stderr,
|
Stderr: stdErr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) {
|
|||||||
Env: r.ReplaceVars(origTask.Env),
|
Env: r.ReplaceVars(origTask.Env),
|
||||||
Silent: origTask.Silent,
|
Silent: origTask.Silent,
|
||||||
Method: r.Replace(origTask.Method),
|
Method: r.Replace(origTask.Method),
|
||||||
|
Prefix: r.Replace(origTask.Prefix),
|
||||||
}
|
}
|
||||||
new.Dir, err = homedir.Expand(new.Dir)
|
new.Dir, err = homedir.Expand(new.Dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -47,6 +48,9 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) {
|
|||||||
if e.Dir != "" && !filepath.IsAbs(new.Dir) {
|
if e.Dir != "" && !filepath.IsAbs(new.Dir) {
|
||||||
new.Dir = filepath.Join(e.Dir, new.Dir)
|
new.Dir = filepath.Join(e.Dir, new.Dir)
|
||||||
}
|
}
|
||||||
|
if new.Prefix == "" {
|
||||||
|
new.Prefix = new.Task
|
||||||
|
}
|
||||||
for k, v := range new.Env {
|
for k, v := range new.Env {
|
||||||
static, err := e.Compiler.HandleDynamicVar(v)
|
static, err := e.Compiler.HandleDynamicVar(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Reference in New Issue
Block a user