Skip to content

Commit

Permalink
Added suport for multiline variables from sh
Browse files Browse the repository at this point in the history
Instead of giving an error on multiline results from sh, the results are
now concatenated to one line by default, separated by a single space.
Trailing and leading white space are stripped like before.

In adition, some flags have been added for advanced use cases to turn
off line concationaton and/or trimming off leading/trailing white-space.
  • Loading branch information
smyrman committed Sep 3, 2017
1 parent 36f3be9 commit 1c29cc9
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 66 deletions.
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,8 @@ set-message:

#### Dynamic variables

The below syntax (`sh:` prop in a variable) is considered a dynamic
variable. The value will be treated as a command and the output assigned.
The below syntax (`sh:` prop in a variable) is considered a dynamic variable.
The value will be treated as a command and the output assigned.

```yml
build:
Expand All @@ -328,6 +328,27 @@ build:

This works for all types of variables.

By default, the result of the shell command is concatenated to one line, and all
trailing and leading white-space are trimmed. This makes the variable somewhat
safer to use in shell commands, and should work well in most use cases.

For advanced use cases, you may set the `multiline` and/or `notrim` flags. By
doing this, you *must* ensure quoting is used in a safe manner when you use the
variable to not break the shell command into two or more commands. The same is
true for multi-line values supplied via yaml.


```yml
build:
cmds:
- go build -ldflags='-X main.History="{{.GIT_HISTORY | replace "'" "\'"| replace """ "\""}}"' main.go
vars:
GIT_COMMIT:
sh: git log -n 5
multiline: true
notrim: true
```

> It's also possible to prefix the variable with `$` to have a dynamic
variable, but this is now considered deprecated:

Expand All @@ -345,7 +366,7 @@ GIT_COMMIT: $git log -n 1 --format=%h
### Go's template engine

Task parse commands as [Go's template engine][gotemplate] before executing
them. Variables are acessible through dot syntax (`.VARNAME`).
them. Variables are accessible through dot syntax (`.VARNAME`).

All functions by the Go's [sprig lib](http://masterminds.github.io/sprig/)
are available. The following example gets the current date in a given format:
Expand Down
20 changes: 20 additions & 0 deletions task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,26 @@ func TestVars(t *testing.T) {
tt.Target = "hello"
tt.Run(t)
}
func TestMultilineVars(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/vars/multiline",
Target: "default",
TrimSpace: false,
Files: map[string]string{
// Note: cat adds a trailing newline.

"echo_nocache.txt": "foo bar\n",
"echo_cache.txt": "foo\nbar\n",

"var_multiline.txt": "\n\nfoo\n bar\nfoobar\n\nbaz\n\n\n",
"sh_default.txt": "foo bar foobar baz\n",
"sh_notrim.txt": " foo bar foobar baz \n",
"sh_multiline.txt": "foo\n bar\nfoobar\n\nbaz\n",
"sh_multiline_notrim.txt": "\n\nfoo\n bar\nfoobar\n\nbaz\n\n\n",
},
}
tt.Run(t)
}

func TestVarsInvalidTmpl(t *testing.T) {
const (
Expand Down
50 changes: 50 additions & 0 deletions testdata/vars/multiline/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
default:
vars:
MULTILINE: "\n\nfoo\n bar\nfoobar\n\nbaz\n\n"
cmds:
- task: file
vars:
CONTENT:
sh: "echo -n 'foo\nbar'"
FILE: "echo_nocache.txt"
- task: file
vars:
CONTENT:
sh: "echo -n 'foo\nbar'"
multiline: true
FILE: "echo_cache.txt"
- task: file
vars:
CONTENT: "{{.MULTILINE}}"
FILE: "var_multiline.txt"
- task: file
vars:
CONTENT:
sh: "echo -n '{{.MULTILINE}}'"
FILE: "sh_default.txt"
- task: file
vars:
CONTENT:
sh: "echo -n '{{.MULTILINE}}'"
notrim: true
FILE: "sh_notrim.txt"
- task: file
vars:
CONTENT:
sh: "echo -n '{{.MULTILINE}}'"
multiline: true
FILE: "sh_multiline.txt"
- task: file
vars:
CONTENT:
sh: "echo -n '{{.MULTILINE}}'"
multiline: true
notrim: true
FILE: "sh_multiline_notrim.txt"

file:
cmds:
- |
cat << EOF > '{{.FILE}}'
{{.CONTENT}}
EOF
156 changes: 93 additions & 63 deletions variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,36 @@ import (
)

var (
// TaskvarsFilePath file containing additional variables
// TaskvarsFilePath file containing additional variables.
TaskvarsFilePath = "Taskvars"
// ErrMultilineResultCmd is returned when a command returns multiline result
ErrMultilineResultCmd = errors.New("Got multiline result from command")
)

// Vars is a string[string] variables map
var (
// ErrCantUnmarshalVar is returned for invalid var YAML.
ErrCantUnmarshalVar = errors.New("task: can't unmarshal var value")
)

// Vars is a string[string] variables map.
type Vars map[string]Var

// Var represents either a static or dynamic variable
type Var struct {
Static string
Sh string
func getEnvironmentVariables() Vars {
var (
env = os.Environ()
m = make(Vars, len(env))
)

for _, e := range env {
keyVal := strings.SplitN(e, "=", 2)
key, val := keyVal[0], keyVal[1]
m[key] = Var{Static: val}
}
return m
}

func (vs Vars) toStringMap() (m map[string]string) {
m = make(map[string]string, len(vs))
for k, v := range vs {
if v.Sh != "" {
if v.Sh != nil {
// Dynamic variable is not yet resolved; trigger
// <no value> to be used in templates.
continue
Expand All @@ -43,30 +54,56 @@ func (vs Vars) toStringMap() (m map[string]string) {
return
}

var (
// ErrCantUnmarshalVar is returned for invalid var YAML
ErrCantUnmarshalVar = errors.New("task: can't unmarshal var value")
)
// shVar is used internally to parse sh values and their results.
type shVar struct {
Sh string `yaml:"sh"`
Multiline bool `yaml:"multiline"`
NoTrimSpace bool `yaml:"notrim"`
}

// compiled returs a copy of sh with template values resolved or nil if sh is
// nil or there was an error in r.
func (sh *shVar) compiled(r *varReplacer) *shVar {
if sh == nil {
return nil
}
s := r.replace(sh.Sh)
if r.err != nil {
return nil
}
return &shVar{
Sh: s,
Multiline: sh.Multiline,
NoTrimSpace: sh.NoTrimSpace,
}
}

// Var represents either a static or dynamic variable.
type Var struct {
Static string
Sh *shVar
}

// UnmarshalYAML implements yaml.Unmarshaler interface
// UnmarshalYAML implements yaml.Unmarshaler interface.
func (v *Var) UnmarshalYAML(unmarshal func(interface{}) error) error {
var str string
if err := unmarshal(&str); err == nil {
if strings.HasPrefix(str, "$") {
v.Sh = strings.TrimPrefix(str, "$")
v.Sh = &shVar{
Sh: strings.TrimPrefix(str, "$"),
}
} else {
v.Static = str
}
return nil
}

var sh struct {
Sh string
}
var sh shVar
if err := unmarshal(&sh); err == nil {
v.Sh = sh.Sh
v.Sh = &sh
return nil
}

return ErrCantUnmarshalVar
}

Expand Down Expand Up @@ -100,11 +137,11 @@ func init() {
}
}

// getVariables returns fully resolved variables following the priorty order:
// 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 environement variables
// - call, taskvars and environment variables
// 4. Taskvars variables, resolved with access to:
// - environment variables
func (e *Executor) getVariables(call Call) (Vars, error) {
Expand Down Expand Up @@ -139,7 +176,7 @@ func (e *Executor) getVariables(call Call) (Vars, error) {
v := dest[k]
dest[k] = Var{
Static: r.replace(v.Static),
Sh: r.replace(v.Sh),
Sh: v.Sh.compiled(&r),
}
}
return r.err
Expand Down Expand Up @@ -179,55 +216,48 @@ func (e *Executor) getVariables(call Call) (Vars, error) {
return result, nil
}

func getEnvironmentVariables() Vars {
var (
env = os.Environ()
m = make(Vars, len(env))
)

for _, e := range env {
keyVal := strings.SplitN(e, "=", 2)
key, val := keyVal[0], keyVal[1]
m[key] = Var{Static: val}
}
return m
}

func (e *Executor) handleShVar(v Var) (string, error) {
if v.Static != "" {
if v.Static != "" || v.Sh == nil {
return v.Static, nil
}

e.muDynamicCache.Lock()
defer e.muDynamicCache.Unlock()
if result, ok := e.dynamicCache[v.Sh]; ok {
return result, nil
}
result, ok := e.dynamicCache[v.Sh.Sh]

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}
}
if !ok {
var stdout bytes.Buffer
opts := &execext.RunCommandOptions{
Command: v.Sh.Sh,
Dir: e.Dir,
Stdout: &stdout,
Stderr: e.Stderr,
}
if err := execext.RunCommand(opts); err != nil {
return "", &dynamicVarError{cause: err, cmd: opts.Command}
}

result := strings.TrimSuffix(stdout.String(), "\n")
if strings.ContainsRune(result, '\n') {
return "", ErrMultilineResultCmd
result = stdout.String()

// Always store raw value in cache!
e.dynamicCache[v.Sh.Sh] = result
e.verbosePrintln(`task: dynamic variable: `, v.Sh.Sh)
}

result = strings.TrimSpace(result)
e.verbosePrintfln(`task: dynamic variable: "%s", result: "%s"`, v.Sh, result)
e.dynamicCache[v.Sh] = result
// Format value for return.
if !v.Sh.NoTrimSpace {
// Trim leading and trailing white-space if nothing else was specificed.
result = strings.TrimSpace(result)
}
if !v.Sh.Multiline {
// Concat to one line if nothing else was specifice.
result = strings.Replace(result, "\r\n", " ", -1)
result = strings.Replace(result, "\n", " ", -1)
}
return result, nil
}

// CompiledTask returns a copy of a task, but replacing
// variables in almost all properties using the Go template package
// CompiledTask returns a copy of a task, but replacing variables in almost all
// properties using the Go template package.
func (e *Executor) CompiledTask(call Call) (*Task, error) {
origTask, ok := e.Tasks[call.Task]
if !ok {
Expand Down Expand Up @@ -289,9 +319,9 @@ func (e *Executor) CompiledTask(call Call) (*Task, error) {
}

// 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.
// 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 Vars
strMap map[string]string
Expand Down Expand Up @@ -342,7 +372,7 @@ func (r *varReplacer) replaceVars(vars Vars) Vars {
for k, v := range vars {
new[k] = Var{
Static: r.replace(v.Static),
Sh: r.replace(v.Sh),
Sh: v.Sh.compiled(r),
}
}
return new
Expand Down

0 comments on commit 1c29cc9

Please sign in to comment.