mirror of
				https://github.com/go-task/task.git
				synced 2025-10-30 23:58:01 +02:00 
			
		
		
		
	Extract some functionality to its own packages
Like variable and template handling, and logging
This commit is contained in:
		| @@ -5,6 +5,10 @@ GO_PACKAGES: | ||||
|   . | ||||
|   ./cmd/task | ||||
|   ./internal/args | ||||
|   ./internal/compiler | ||||
|   ./internal/compiler/v1 | ||||
|   ./internal/execext | ||||
|   ./internal/logger | ||||
|   ./internal/status | ||||
|   ./internal/taskfile | ||||
|   ./internal/templater | ||||
|   | ||||
| @@ -51,15 +51,6 @@ func (err *cantWatchNoSourcesError) Error() string { | ||||
| 	return fmt.Sprintf(`task: Can't watch task "%s" because it has no specified sources`, err.taskName) | ||||
| } | ||||
|  | ||||
| type dynamicVarError struct { | ||||
| 	cause error | ||||
| 	cmd   string | ||||
| } | ||||
|  | ||||
| func (err *dynamicVarError) Error() string { | ||||
| 	return fmt.Sprintf(`task: Command "%s" in taskvars file failed: %s`, err.cmd, err.cause) | ||||
| } | ||||
|  | ||||
| // MaximumTaskCallExceededError is returned when a task is called too | ||||
| // many times. In this case you probably have a cyclic dependendy or | ||||
| // infinite loop | ||||
|   | ||||
							
								
								
									
										4
									
								
								help.go
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								help.go
									
									
									
									
									
								
							| @@ -12,10 +12,10 @@ import ( | ||||
| func (e *Executor) PrintTasksHelp() { | ||||
| 	tasks := e.tasksWithDesc() | ||||
| 	if len(tasks) == 0 { | ||||
| 		e.outf("task: No tasks with description available") | ||||
| 		e.Logger.Outf("task: No tasks with description available") | ||||
| 		return | ||||
| 	} | ||||
| 	e.outf("task: Available tasks for this project:") | ||||
| 	e.Logger.Outf("task: Available tasks for this project:") | ||||
|  | ||||
| 	// Format in tab-separated columns with a tab stop of 8. | ||||
| 	w := tabwriter.NewWriter(e.Stdout, 0, 8, 0, '\t', 0) | ||||
|   | ||||
							
								
								
									
										12
									
								
								internal/compiler/compiler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								internal/compiler/compiler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| package compiler | ||||
|  | ||||
| import ( | ||||
| 	"github.com/go-task/task/internal/taskfile" | ||||
| ) | ||||
|  | ||||
| // Compiler handles compilation of a task before its execution. | ||||
| // E.g. variable merger, template processing, etc. | ||||
| type Compiler interface { | ||||
| 	GetVariables(t *taskfile.Task, call taskfile.Call) (taskfile.Vars, error) | ||||
| 	HandleDynamicVar(v taskfile.Var) (string, error) | ||||
| } | ||||
							
								
								
									
										24
									
								
								internal/compiler/env.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								internal/compiler/env.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| package compiler | ||||
|  | ||||
| import ( | ||||
| 	"os" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/go-task/task/internal/taskfile" | ||||
| ) | ||||
|  | ||||
| // GetEnviron the all return all environment variables encapsulated on a | ||||
| // taskfile.Vars | ||||
| func GetEnviron() taskfile.Vars { | ||||
| 	var ( | ||||
| 		env = os.Environ() | ||||
| 		m   = make(taskfile.Vars, len(env)) | ||||
| 	) | ||||
|  | ||||
| 	for _, e := range env { | ||||
| 		keyVal := strings.SplitN(e, "=", 2) | ||||
| 		key, val := keyVal[0], keyVal[1] | ||||
| 		m[key] = taskfile.Var{Static: val} | ||||
| 	} | ||||
| 	return m | ||||
| } | ||||
							
								
								
									
										145
									
								
								internal/compiler/v1/compiler_v1.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								internal/compiler/v1/compiler_v1.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,145 @@ | ||||
| package v1 | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/go-task/task/internal/compiler" | ||||
| 	"github.com/go-task/task/internal/execext" | ||||
| 	"github.com/go-task/task/internal/logger" | ||||
| 	"github.com/go-task/task/internal/taskfile" | ||||
| 	"github.com/go-task/task/internal/templater" | ||||
| ) | ||||
|  | ||||
| var _ compiler.Compiler = &CompilerV1{} | ||||
|  | ||||
| type CompilerV1 struct { | ||||
| 	Dir  string | ||||
| 	Vars taskfile.Vars | ||||
|  | ||||
| 	Logger *logger.Logger | ||||
|  | ||||
| 	dynamicCache   map[string]string | ||||
| 	muDynamicCache sync.Mutex | ||||
| } | ||||
|  | ||||
| // GetVariables returns fully resolved variables following the priority order: | ||||
| // 1. Call variables (should already have been resolved) | ||||
| // 2. Environment (should not need to be resolved) | ||||
| // 3. Task variables, resolved with access to: | ||||
| //    - call, taskvars and environment variables | ||||
| // 4. Taskvars variables, resolved with access to: | ||||
| //    - environment variables | ||||
| func (c *CompilerV1) GetVariables(t *taskfile.Task, call taskfile.Call) (taskfile.Vars, error) { | ||||
| 	merge := func(dest taskfile.Vars, srcs ...taskfile.Vars) { | ||||
| 		for _, src := range srcs { | ||||
| 			for k, v := range src { | ||||
| 				dest[k] = v | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	varsKeys := func(srcs ...taskfile.Vars) []string { | ||||
| 		m := make(map[string]struct{}) | ||||
| 		for _, src := range srcs { | ||||
| 			for k := range src { | ||||
| 				m[k] = struct{}{} | ||||
| 			} | ||||
| 		} | ||||
| 		lst := make([]string, 0, len(m)) | ||||
| 		for k := range m { | ||||
| 			lst = append(lst, k) | ||||
| 		} | ||||
| 		return lst | ||||
| 	} | ||||
| 	replaceVars := func(dest taskfile.Vars, keys []string) error { | ||||
| 		r := templater.Templater{Vars: dest} | ||||
| 		for _, k := range keys { | ||||
| 			v := dest[k] | ||||
| 			dest[k] = taskfile.Var{ | ||||
| 				Static: r.Replace(v.Static), | ||||
| 				Sh:     r.Replace(v.Sh), | ||||
| 			} | ||||
| 		} | ||||
| 		return r.Err() | ||||
| 	} | ||||
| 	resolveShell := func(dest taskfile.Vars, keys []string) error { | ||||
| 		for _, k := range keys { | ||||
| 			v := dest[k] | ||||
| 			static, err := c.HandleDynamicVar(v) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			dest[k] = taskfile.Var{Static: static} | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
| 	update := func(dest taskfile.Vars, srcs ...taskfile.Vars) error { | ||||
| 		merge(dest, srcs...) | ||||
| 		// updatedKeys ensures template evaluation is run only once. | ||||
| 		updatedKeys := varsKeys(srcs...) | ||||
| 		if err := replaceVars(dest, updatedKeys); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		return resolveShell(dest, updatedKeys) | ||||
| 	} | ||||
|  | ||||
| 	// Resolve taskvars variables to "result" with environment override variables. | ||||
| 	override := compiler.GetEnviron() | ||||
| 	result := make(taskfile.Vars, len(c.Vars)+len(t.Vars)+len(override)) | ||||
| 	if err := update(result, c.Vars, override); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	// Resolve task variables to "result" with environment and call override variables. | ||||
| 	merge(override, call.Vars) | ||||
| 	if err := update(result, t.Vars, override); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| func (c *CompilerV1) HandleDynamicVar(v taskfile.Var) (string, error) { | ||||
| 	if v.Static != "" || v.Sh == "" { | ||||
| 		return v.Static, nil | ||||
| 	} | ||||
|  | ||||
| 	c.muDynamicCache.Lock() | ||||
| 	defer c.muDynamicCache.Unlock() | ||||
|  | ||||
| 	if c.dynamicCache == nil { | ||||
| 		c.dynamicCache = make(map[string]string, 30) | ||||
| 	} | ||||
| 	if result, ok := c.dynamicCache[v.Sh]; ok { | ||||
| 		return result, nil | ||||
| 	} | ||||
|  | ||||
| 	var stdout bytes.Buffer | ||||
| 	opts := &execext.RunCommandOptions{ | ||||
| 		Command: v.Sh, | ||||
| 		Dir:     c.Dir, | ||||
| 		Stdout:  &stdout, | ||||
| 		Stderr:  c.Logger.Stderr, | ||||
| 	} | ||||
| 	if err := execext.RunCommand(opts); err != nil { | ||||
| 		return "", &dynamicVarError{cause: err, cmd: opts.Command} | ||||
| 	} | ||||
|  | ||||
| 	// Trim a single trailing newline from the result to make most command | ||||
| 	// output easier to use in shell commands. | ||||
| 	result := strings.TrimSuffix(stdout.String(), "\n") | ||||
|  | ||||
| 	c.dynamicCache[v.Sh] = result | ||||
| 	c.Logger.VerboseErrf(`task: dynamic variable: '%s' result: '%s'`, v.Sh, result) | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| type dynamicVarError struct { | ||||
| 	cause error | ||||
| 	cmd   string | ||||
| } | ||||
|  | ||||
| func (err *dynamicVarError) Error() string { | ||||
| 	return fmt.Sprintf(`task: Command "%s" in taskvars file failed: %s`, err.cmd, err.cause) | ||||
| } | ||||
							
								
								
									
										38
									
								
								internal/logger/logger.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								internal/logger/logger.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| package logger | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| ) | ||||
|  | ||||
| type Logger struct { | ||||
| 	Stdout  io.Writer | ||||
| 	Stderr  io.Writer | ||||
| 	Verbose bool | ||||
| } | ||||
|  | ||||
| func (l *Logger) Outf(s string, args ...interface{}) { | ||||
| 	if len(args) == 0 { | ||||
| 		s, args = "%s", []interface{}{s} | ||||
| 	} | ||||
| 	fmt.Fprintf(l.Stdout, s+"\n", args...) | ||||
| } | ||||
|  | ||||
| func (l *Logger) VerboseOutf(s string, args ...interface{}) { | ||||
| 	if l.Verbose { | ||||
| 		l.Outf(s, args...) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (l *Logger) Errf(s string, args ...interface{}) { | ||||
| 	if len(args) == 0 { | ||||
| 		s, args = "%s", []interface{}{s} | ||||
| 	} | ||||
| 	fmt.Fprintf(l.Stderr, s+"\n", args...) | ||||
| } | ||||
|  | ||||
| func (l *Logger) VerboseErrf(s string, args ...interface{}) { | ||||
| 	if l.Verbose { | ||||
| 		l.Errf(s, args...) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										52
									
								
								internal/templater/funcs.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								internal/templater/funcs.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| package templater | ||||
|  | ||||
| import ( | ||||
| 	"path/filepath" | ||||
| 	"runtime" | ||||
| 	"strings" | ||||
| 	"text/template" | ||||
|  | ||||
| 	"github.com/Masterminds/sprig" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	templateFuncs template.FuncMap | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	taskFuncs := template.FuncMap{ | ||||
| 		"OS":   func() string { return runtime.GOOS }, | ||||
| 		"ARCH": func() string { return runtime.GOARCH }, | ||||
| 		"catLines": func(s string) string { | ||||
| 			s = strings.Replace(s, "\r\n", " ", -1) | ||||
| 			return strings.Replace(s, "\n", " ", -1) | ||||
| 		}, | ||||
| 		"splitLines": func(s string) []string { | ||||
| 			s = strings.Replace(s, "\r\n", "\n", -1) | ||||
| 			return strings.Split(s, "\n") | ||||
| 		}, | ||||
| 		"fromSlash": func(path string) string { | ||||
| 			return filepath.FromSlash(path) | ||||
| 		}, | ||||
| 		"toSlash": func(path string) string { | ||||
| 			return filepath.ToSlash(path) | ||||
| 		}, | ||||
| 		"exeExt": func() string { | ||||
| 			if runtime.GOOS == "windows" { | ||||
| 				return ".exe" | ||||
| 			} | ||||
| 			return "" | ||||
| 		}, | ||||
| 		// IsSH is deprecated. | ||||
| 		"IsSH": func() bool { return true }, | ||||
| 	} | ||||
| 	// Deprecated aliases for renamed functions. | ||||
| 	taskFuncs["FromSlash"] = taskFuncs["fromSlash"] | ||||
| 	taskFuncs["ToSlash"] = taskFuncs["toSlash"] | ||||
| 	taskFuncs["ExeExt"] = taskFuncs["exeExt"] | ||||
|  | ||||
| 	templateFuncs = sprig.TxtFuncMap() | ||||
| 	for k, v := range taskFuncs { | ||||
| 		templateFuncs[k] = v | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										73
									
								
								internal/templater/templater.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								internal/templater/templater.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| package templater | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"text/template" | ||||
|  | ||||
| 	"github.com/go-task/task/internal/taskfile" | ||||
| ) | ||||
|  | ||||
| // Templater is a help struct that allow us to call "replaceX" funcs multiple | ||||
| // times, without having to check for error each time. The first error that | ||||
| // happen will be assigned to r.err, and consecutive calls to funcs will just | ||||
| // return the zero value. | ||||
| type Templater struct { | ||||
| 	Vars taskfile.Vars | ||||
|  | ||||
| 	strMap map[string]string | ||||
| 	err    error | ||||
| } | ||||
|  | ||||
| func (r *Templater) Replace(str string) string { | ||||
| 	if r.err != nil || str == "" { | ||||
| 		return "" | ||||
| 	} | ||||
|  | ||||
| 	templ, err := template.New("").Funcs(templateFuncs).Parse(str) | ||||
| 	if err != nil { | ||||
| 		r.err = err | ||||
| 		return "" | ||||
| 	} | ||||
|  | ||||
| 	if r.strMap == nil { | ||||
| 		r.strMap = r.Vars.ToStringMap() | ||||
| 	} | ||||
|  | ||||
| 	var b bytes.Buffer | ||||
| 	if err = templ.Execute(&b, r.strMap); err != nil { | ||||
| 		r.err = err | ||||
| 		return "" | ||||
| 	} | ||||
| 	return b.String() | ||||
| } | ||||
|  | ||||
| func (r *Templater) ReplaceSlice(strs []string) []string { | ||||
| 	if r.err != nil || len(strs) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	new := make([]string, len(strs)) | ||||
| 	for i, str := range strs { | ||||
| 		new[i] = r.Replace(str) | ||||
| 	} | ||||
| 	return new | ||||
| } | ||||
|  | ||||
| func (r *Templater) ReplaceVars(vars taskfile.Vars) taskfile.Vars { | ||||
| 	if r.err != nil || len(vars) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	new := make(taskfile.Vars, len(vars)) | ||||
| 	for k, v := range vars { | ||||
| 		new[k] = taskfile.Var{ | ||||
| 			Static: r.Replace(v.Static), | ||||
| 			Sh:     r.Replace(v.Sh), | ||||
| 		} | ||||
| 	} | ||||
| 	return new | ||||
| } | ||||
|  | ||||
| func (r *Templater) Err() error { | ||||
| 	return r.err | ||||
| } | ||||
							
								
								
									
										31
									
								
								log.go
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								log.go
									
									
									
									
									
								
							| @@ -1,31 +0,0 @@ | ||||
| package task | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| ) | ||||
|  | ||||
| func (e *Executor) outf(s string, args ...interface{}) { | ||||
| 	if len(args) == 0 { | ||||
| 		s, args = "%s", []interface{}{s} | ||||
| 	} | ||||
| 	fmt.Fprintf(e.Stdout, s+"\n", args...) | ||||
| } | ||||
|  | ||||
| func (e *Executor) verboseOutf(s string, args ...interface{}) { | ||||
| 	if e.Verbose { | ||||
| 		e.outf(s, args...) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (e *Executor) errf(s string, args ...interface{}) { | ||||
| 	if len(args) == 0 { | ||||
| 		s, args = "%s", []interface{}{s} | ||||
| 	} | ||||
| 	fmt.Fprintf(e.Stderr, s+"\n", args...) | ||||
| } | ||||
|  | ||||
| func (e *Executor) verboseErrf(s string, args ...interface{}) { | ||||
| 	if e.Verbose { | ||||
| 		e.errf(s, args...) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										35
									
								
								task.go
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								task.go
									
									
									
									
									
								
							| @@ -5,10 +5,12 @@ import ( | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"sync" | ||||
| 	"sync/atomic" | ||||
|  | ||||
| 	"github.com/go-task/task/internal/compiler" | ||||
| 	compilerv1 "github.com/go-task/task/internal/compiler/v1" | ||||
| 	"github.com/go-task/task/internal/execext" | ||||
| 	"github.com/go-task/task/internal/logger" | ||||
| 	"github.com/go-task/task/internal/taskfile" | ||||
|  | ||||
| 	"golang.org/x/sync/errgroup" | ||||
| @@ -37,12 +39,12 @@ type Executor struct { | ||||
| 	Stdout io.Writer | ||||
| 	Stderr io.Writer | ||||
|  | ||||
| 	Logger   *logger.Logger | ||||
| 	Compiler compiler.Compiler | ||||
|  | ||||
| 	taskvars taskfile.Vars | ||||
|  | ||||
| 	taskCallCount map[string]*int32 | ||||
|  | ||||
| 	dynamicCache   map[string]string | ||||
| 	muDynamicCache sync.Mutex | ||||
| } | ||||
|  | ||||
| // Run runs Task | ||||
| @@ -59,16 +61,27 @@ func (e *Executor) Run(calls ...taskfile.Call) error { | ||||
| 	if e.Stderr == nil { | ||||
| 		e.Stderr = os.Stderr | ||||
| 	} | ||||
| 	if e.Logger == nil { | ||||
| 		e.Logger = &logger.Logger{ | ||||
| 			Stdout:  e.Stdout, | ||||
| 			Stderr:  e.Stderr, | ||||
| 			Verbose: e.Verbose, | ||||
| 		} | ||||
| 	} | ||||
| 	// TODO: Add version 2 | ||||
| 	if e.Compiler == nil { | ||||
| 		e.Compiler = &compilerv1.CompilerV1{ | ||||
| 			Dir:    e.Dir, | ||||
| 			Vars:   e.taskvars, | ||||
| 			Logger: e.Logger, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks)) | ||||
| 	for k := range e.Taskfile.Tasks { | ||||
| 		e.taskCallCount[k] = new(int32) | ||||
| 	} | ||||
|  | ||||
| 	if e.dynamicCache == nil { | ||||
| 		e.dynamicCache = make(map[string]string, 10) | ||||
| 	} | ||||
|  | ||||
| 	// check if given tasks exist | ||||
| 	for _, c := range calls { | ||||
| 		if _, ok := e.Taskfile.Tasks[c.Task]; !ok { | ||||
| @@ -111,7 +124,7 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error { | ||||
| 		} | ||||
| 		if upToDate { | ||||
| 			if !e.Silent { | ||||
| 				e.errf(`task: Task "%s" is up to date`, t.Task) | ||||
| 				e.Logger.Errf(`task: Task "%s" is up to date`, t.Task) | ||||
| 			} | ||||
| 			return nil | ||||
| 		} | ||||
| @@ -120,7 +133,7 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error { | ||||
| 	for i := range t.Cmds { | ||||
| 		if err := e.runCommand(ctx, t, call, i); err != nil { | ||||
| 			if err2 := statusOnError(t); err2 != nil { | ||||
| 				e.verboseErrf("task: error cleaning status on error: %v", err2) | ||||
| 				e.Logger.VerboseErrf("task: error cleaning status on error: %v", err2) | ||||
| 			} | ||||
| 			return &taskRunError{t.Task, err} | ||||
| 		} | ||||
| @@ -150,7 +163,7 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi | ||||
| 	} | ||||
|  | ||||
| 	if e.Verbose || (!cmd.Silent && !t.Silent && !e.Silent) { | ||||
| 		e.errf(cmd.Cmd) | ||||
| 		e.Logger.Errf(cmd.Cmd) | ||||
| 	} | ||||
|  | ||||
| 	return execext.RunCommand(&execext.RunCommandOptions{ | ||||
|   | ||||
| @@ -209,10 +209,12 @@ func TestStatus(t *testing.T) { | ||||
| 		t.Errorf("File should not exists: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	var buff bytes.Buffer | ||||
| 	e := &task.Executor{ | ||||
| 		Dir:    dir, | ||||
| 		Stdout: ioutil.Discard, | ||||
| 		Stderr: ioutil.Discard, | ||||
| 		Stdout: &buff, | ||||
| 		Stderr: &buff, | ||||
| 		Silent: true, | ||||
| 	} | ||||
| 	assert.NoError(t, e.ReadTaskfile()) | ||||
| 	assert.NoError(t, e.Run(taskfile.Call{Task: "gen-foo"})) | ||||
| @@ -221,8 +223,7 @@ func TestStatus(t *testing.T) { | ||||
| 		t.Errorf("File should exists: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	buff := bytes.NewBuffer(nil) | ||||
| 	e.Stdout, e.Stderr = buff, buff | ||||
| 	e.Silent = false | ||||
| 	assert.NoError(t, e.Run(taskfile.Call{Task: "gen-foo"})) | ||||
|  | ||||
| 	if buff.String() != `task: Task "gen-foo" is up to date`+"\n" { | ||||
|   | ||||
							
								
								
									
										267
									
								
								variables.go
									
									
									
									
									
								
							
							
						
						
									
										267
									
								
								variables.go
									
									
									
									
									
								
							| @@ -1,17 +1,11 @@ | ||||
| package task | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"runtime" | ||||
| 	"strings" | ||||
| 	"text/template" | ||||
|  | ||||
| 	"github.com/go-task/task/internal/execext" | ||||
| 	"github.com/go-task/task/internal/taskfile" | ||||
| 	"github.com/go-task/task/internal/templater" | ||||
|  | ||||
| 	"github.com/Masterminds/sprig" | ||||
| 	"github.com/mitchellh/go-homedir" | ||||
| ) | ||||
|  | ||||
| @@ -20,131 +14,6 @@ var ( | ||||
| 	TaskvarsFilePath = "Taskvars" | ||||
| ) | ||||
|  | ||||
| func getEnvironmentVariables() taskfile.Vars { | ||||
| 	var ( | ||||
| 		env = os.Environ() | ||||
| 		m   = make(taskfile.Vars, len(env)) | ||||
| 	) | ||||
|  | ||||
| 	for _, e := range env { | ||||
| 		keyVal := strings.SplitN(e, "=", 2) | ||||
| 		key, val := keyVal[0], keyVal[1] | ||||
| 		m[key] = taskfile.Var{Static: val} | ||||
| 	} | ||||
| 	return m | ||||
| } | ||||
|  | ||||
| // getVariables returns fully resolved variables following the priority order: | ||||
| // 1. Call variables (should already have been resolved) | ||||
| // 2. Environment (should not need to be resolved) | ||||
| // 3. Task variables, resolved with access to: | ||||
| //    - call, taskvars and environment variables | ||||
| // 4. Taskvars variables, resolved with access to: | ||||
| //    - environment variables | ||||
| func (e *Executor) getVariables(call taskfile.Call) (taskfile.Vars, error) { | ||||
| 	t, ok := e.Taskfile.Tasks[call.Task] | ||||
| 	if !ok { | ||||
| 		return nil, &taskNotFoundError{call.Task} | ||||
| 	} | ||||
|  | ||||
| 	merge := func(dest taskfile.Vars, srcs ...taskfile.Vars) { | ||||
| 		for _, src := range srcs { | ||||
| 			for k, v := range src { | ||||
| 				dest[k] = v | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	varsKeys := func(srcs ...taskfile.Vars) []string { | ||||
| 		m := make(map[string]struct{}) | ||||
| 		for _, src := range srcs { | ||||
| 			for k := range src { | ||||
| 				m[k] = struct{}{} | ||||
| 			} | ||||
| 		} | ||||
| 		lst := make([]string, 0, len(m)) | ||||
| 		for k := range m { | ||||
| 			lst = append(lst, k) | ||||
| 		} | ||||
| 		return lst | ||||
| 	} | ||||
| 	replaceVars := func(dest taskfile.Vars, keys []string) error { | ||||
| 		r := varReplacer{vars: dest} | ||||
| 		for _, k := range keys { | ||||
| 			v := dest[k] | ||||
| 			dest[k] = taskfile.Var{ | ||||
| 				Static: r.replace(v.Static), | ||||
| 				Sh:     r.replace(v.Sh), | ||||
| 			} | ||||
| 		} | ||||
| 		return r.err | ||||
| 	} | ||||
| 	resolveShell := func(dest taskfile.Vars, keys []string) error { | ||||
| 		for _, k := range keys { | ||||
| 			v := dest[k] | ||||
| 			static, err := e.handleShVar(v) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			dest[k] = taskfile.Var{Static: static} | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
| 	update := func(dest taskfile.Vars, srcs ...taskfile.Vars) error { | ||||
| 		merge(dest, srcs...) | ||||
| 		// updatedKeys ensures template evaluation is run only once. | ||||
| 		updatedKeys := varsKeys(srcs...) | ||||
| 		if err := replaceVars(dest, updatedKeys); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		return resolveShell(dest, updatedKeys) | ||||
| 	} | ||||
|  | ||||
| 	// Resolve taskvars variables to "result" with environment override variables. | ||||
| 	override := getEnvironmentVariables() | ||||
| 	result := make(taskfile.Vars, len(e.taskvars)+len(t.Vars)+len(override)) | ||||
| 	if err := update(result, e.taskvars, override); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	// Resolve task variables to "result" with environment and call override variables. | ||||
| 	merge(override, call.Vars) | ||||
| 	if err := update(result, t.Vars, override); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| func (e *Executor) handleShVar(v taskfile.Var) (string, error) { | ||||
| 	if v.Static != "" || v.Sh == "" { | ||||
| 		return v.Static, nil | ||||
| 	} | ||||
| 	e.muDynamicCache.Lock() | ||||
| 	defer e.muDynamicCache.Unlock() | ||||
|  | ||||
| 	if result, ok := e.dynamicCache[v.Sh]; ok { | ||||
| 		return result, nil | ||||
| 	} | ||||
|  | ||||
| 	var stdout bytes.Buffer | ||||
| 	opts := &execext.RunCommandOptions{ | ||||
| 		Command: v.Sh, | ||||
| 		Dir:     e.Dir, | ||||
| 		Stdout:  &stdout, | ||||
| 		Stderr:  e.Stderr, | ||||
| 	} | ||||
| 	if err := execext.RunCommand(opts); err != nil { | ||||
| 		return "", &dynamicVarError{cause: err, cmd: opts.Command} | ||||
| 	} | ||||
|  | ||||
| 	// Trim a single trailing newline from the result to make most command | ||||
| 	// output easier to use in shell commands. | ||||
| 	result := strings.TrimSuffix(stdout.String(), "\n") | ||||
|  | ||||
| 	e.dynamicCache[v.Sh] = result | ||||
| 	e.verboseErrf(`task: dynamic variable: '%s' result: '%s'`, v.Sh, result) | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // CompiledTask returns a copy of a task, but replacing variables in almost all | ||||
| // properties using the Go template package. | ||||
| func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) { | ||||
| @@ -153,23 +22,23 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) { | ||||
| 		return nil, &taskNotFoundError{call.Task} | ||||
| 	} | ||||
|  | ||||
| 	vars, err := e.getVariables(call) | ||||
| 	vars, err := e.Compiler.GetVariables(origTask, call) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	r := varReplacer{vars: vars} | ||||
| 	r := templater.Templater{Vars: vars} | ||||
|  | ||||
| 	new := taskfile.Task{ | ||||
| 		Task:      origTask.Task, | ||||
| 		Desc:      r.replace(origTask.Desc), | ||||
| 		Sources:   r.replaceSlice(origTask.Sources), | ||||
| 		Generates: r.replaceSlice(origTask.Generates), | ||||
| 		Status:    r.replaceSlice(origTask.Status), | ||||
| 		Dir:       r.replace(origTask.Dir), | ||||
| 		Desc:      r.Replace(origTask.Desc), | ||||
| 		Sources:   r.ReplaceSlice(origTask.Sources), | ||||
| 		Generates: r.ReplaceSlice(origTask.Generates), | ||||
| 		Status:    r.ReplaceSlice(origTask.Status), | ||||
| 		Dir:       r.Replace(origTask.Dir), | ||||
| 		Vars:      nil, | ||||
| 		Env:       r.replaceVars(origTask.Env), | ||||
| 		Env:       r.ReplaceVars(origTask.Env), | ||||
| 		Silent:    origTask.Silent, | ||||
| 		Method:    r.replace(origTask.Method), | ||||
| 		Method:    r.Replace(origTask.Method), | ||||
| 	} | ||||
| 	new.Dir, err = homedir.Expand(new.Dir) | ||||
| 	if err != nil { | ||||
| @@ -179,7 +48,7 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) { | ||||
| 		new.Dir = filepath.Join(e.Dir, new.Dir) | ||||
| 	} | ||||
| 	for k, v := range new.Env { | ||||
| 		static, err := e.handleShVar(v) | ||||
| 		static, err := e.Compiler.HandleDynamicVar(v) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| @@ -190,10 +59,10 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) { | ||||
| 		new.Cmds = make([]*taskfile.Cmd, len(origTask.Cmds)) | ||||
| 		for i, cmd := range origTask.Cmds { | ||||
| 			new.Cmds[i] = &taskfile.Cmd{ | ||||
| 				Task:   r.replace(cmd.Task), | ||||
| 				Task:   r.Replace(cmd.Task), | ||||
| 				Silent: cmd.Silent, | ||||
| 				Cmd:    r.replace(cmd.Cmd), | ||||
| 				Vars:   r.replaceVars(cmd.Vars), | ||||
| 				Cmd:    r.Replace(cmd.Cmd), | ||||
| 				Vars:   r.ReplaceVars(cmd.Vars), | ||||
| 			} | ||||
|  | ||||
| 		} | ||||
| @@ -202,113 +71,11 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) { | ||||
| 		new.Deps = make([]*taskfile.Dep, len(origTask.Deps)) | ||||
| 		for i, dep := range origTask.Deps { | ||||
| 			new.Deps[i] = &taskfile.Dep{ | ||||
| 				Task: r.replace(dep.Task), | ||||
| 				Vars: r.replaceVars(dep.Vars), | ||||
| 				Task: r.Replace(dep.Task), | ||||
| 				Vars: r.ReplaceVars(dep.Vars), | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return &new, r.err | ||||
| } | ||||
|  | ||||
| // varReplacer is a help struct that allow us to call "replaceX" funcs multiple | ||||
| // times, without having to check for error each time. The first error that | ||||
| // happen will be assigned to r.err, and consecutive calls to funcs will just | ||||
| // return the zero value. | ||||
| type varReplacer struct { | ||||
| 	vars   taskfile.Vars | ||||
| 	strMap map[string]string | ||||
| 	err    error | ||||
| } | ||||
|  | ||||
| func (r *varReplacer) replace(str string) string { | ||||
| 	if r.err != nil || str == "" { | ||||
| 		return "" | ||||
| 	} | ||||
|  | ||||
| 	templ, err := template.New("").Funcs(templateFuncs).Parse(str) | ||||
| 	if err != nil { | ||||
| 		r.err = err | ||||
| 		return "" | ||||
| 	} | ||||
|  | ||||
| 	if r.strMap == nil { | ||||
| 		r.strMap = r.vars.ToStringMap() | ||||
| 	} | ||||
|  | ||||
| 	var b bytes.Buffer | ||||
| 	if err = templ.Execute(&b, r.strMap); err != nil { | ||||
| 		r.err = err | ||||
| 		return "" | ||||
| 	} | ||||
| 	return b.String() | ||||
| } | ||||
|  | ||||
| func (r *varReplacer) replaceSlice(strs []string) []string { | ||||
| 	if r.err != nil || len(strs) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	new := make([]string, len(strs)) | ||||
| 	for i, str := range strs { | ||||
| 		new[i] = r.replace(str) | ||||
| 	} | ||||
| 	return new | ||||
| } | ||||
|  | ||||
| func (r *varReplacer) replaceVars(vars taskfile.Vars) taskfile.Vars { | ||||
| 	if r.err != nil || len(vars) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	new := make(taskfile.Vars, len(vars)) | ||||
| 	for k, v := range vars { | ||||
| 		new[k] = taskfile.Var{ | ||||
| 			Static: r.replace(v.Static), | ||||
| 			Sh:     r.replace(v.Sh), | ||||
| 		} | ||||
| 	} | ||||
| 	return new | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	templateFuncs template.FuncMap | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	taskFuncs := template.FuncMap{ | ||||
| 		"OS":   func() string { return runtime.GOOS }, | ||||
| 		"ARCH": func() string { return runtime.GOARCH }, | ||||
| 		"catLines": func(s string) string { | ||||
| 			s = strings.Replace(s, "\r\n", " ", -1) | ||||
| 			return strings.Replace(s, "\n", " ", -1) | ||||
| 		}, | ||||
| 		"splitLines": func(s string) []string { | ||||
| 			s = strings.Replace(s, "\r\n", "\n", -1) | ||||
| 			return strings.Split(s, "\n") | ||||
| 		}, | ||||
| 		"fromSlash": func(path string) string { | ||||
| 			return filepath.FromSlash(path) | ||||
| 		}, | ||||
| 		"toSlash": func(path string) string { | ||||
| 			return filepath.ToSlash(path) | ||||
| 		}, | ||||
| 		"exeExt": func() string { | ||||
| 			if runtime.GOOS == "windows" { | ||||
| 				return ".exe" | ||||
| 			} | ||||
| 			return "" | ||||
| 		}, | ||||
| 		// IsSH is deprecated. | ||||
| 		"IsSH": func() bool { return true }, | ||||
| 	} | ||||
| 	// Deprecated aliases for renamed functions. | ||||
| 	taskFuncs["FromSlash"] = taskFuncs["fromSlash"] | ||||
| 	taskFuncs["ToSlash"] = taskFuncs["toSlash"] | ||||
| 	taskFuncs["ExeExt"] = taskFuncs["exeExt"] | ||||
|  | ||||
| 	templateFuncs = sprig.TxtFuncMap() | ||||
| 	for k, v := range taskFuncs { | ||||
| 		templateFuncs[k] = v | ||||
| 	} | ||||
| 	return &new, r.Err() | ||||
| } | ||||
|   | ||||
							
								
								
									
										12
									
								
								watch.go
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								watch.go
									
									
									
									
									
								
							| @@ -21,14 +21,14 @@ func (e *Executor) watchTasks(calls ...taskfile.Call) error { | ||||
| 	for i, c := range calls { | ||||
| 		tasks[i] = c.Task | ||||
| 	} | ||||
| 	e.errf("task: Started watching for tasks: %s", strings.Join(tasks, ", ")) | ||||
| 	e.Logger.Errf("task: Started watching for tasks: %s", strings.Join(tasks, ", ")) | ||||
|  | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
| 	for _, c := range calls { | ||||
| 		c := c | ||||
| 		go func() { | ||||
| 			if err := e.RunTask(ctx, c); err != nil && !isContextError(err) { | ||||
| 				e.errf("%v", err) | ||||
| 				e.Logger.Errf("%v", err) | ||||
| 			} | ||||
| 		}() | ||||
| 	} | ||||
| @@ -44,7 +44,7 @@ func (e *Executor) watchTasks(calls ...taskfile.Call) error { | ||||
| 		for { | ||||
| 			select { | ||||
| 			case event := <-w.Event: | ||||
| 				e.verboseErrf("task: received watch event: %v", event) | ||||
| 				e.Logger.VerboseErrf("task: received watch event: %v", event) | ||||
|  | ||||
| 				cancel() | ||||
| 				ctx, cancel = context.WithCancel(context.Background()) | ||||
| @@ -52,7 +52,7 @@ func (e *Executor) watchTasks(calls ...taskfile.Call) error { | ||||
| 					c := c | ||||
| 					go func() { | ||||
| 						if err := e.RunTask(ctx, c); err != nil && !isContextError(err) { | ||||
| 							e.errf("%v", err) | ||||
| 							e.Logger.Errf("%v", err) | ||||
| 						} | ||||
| 					}() | ||||
| 				} | ||||
| @@ -63,7 +63,7 @@ func (e *Executor) watchTasks(calls ...taskfile.Call) error { | ||||
| 						w.TriggerEvent(watcher.Remove, nil) | ||||
| 					}() | ||||
| 				default: | ||||
| 					e.errf("%v", err) | ||||
| 					e.Logger.Errf("%v", err) | ||||
| 				} | ||||
| 			case <-w.Closed: | ||||
| 				return | ||||
| @@ -75,7 +75,7 @@ func (e *Executor) watchTasks(calls ...taskfile.Call) error { | ||||
| 		// re-register each second because we can have new files | ||||
| 		for { | ||||
| 			if err := e.registerWatchedFiles(w, calls...); err != nil { | ||||
| 				e.errf("%v", err) | ||||
| 				e.Logger.Errf("%v", err) | ||||
| 			} | ||||
| 			time.Sleep(time.Second) | ||||
| 		} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user