diff --git a/execext/exec.go b/execext/exec.go index 86328f55..0150e8da 100644 --- a/execext/exec.go +++ b/execext/exec.go @@ -2,22 +2,51 @@ package execext import ( "context" - "os/exec" + "errors" + "io" + "strings" + + "github.com/mvdan/sh/interp" + "github.com/mvdan/sh/syntax" ) +type RunCommandOptions struct { + Context context.Context + Command string + Dir string + Env []string + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer +} + var ( - // ShPath is path to "sh" command - ShPath string - // ShExists is true if "sh" command is available on the system - ShExists bool + // ErrNilOptions is returned when a nil options is given + ErrNilOptions = errors.New("execext: nil options given") ) -func init() { - var err error - ShPath, err = exec.LookPath("sh") - ShExists = err == nil -} +// RunCommand runs a shell command +func RunCommand(opts *RunCommandOptions) error { + if opts == nil { + return ErrNilOptions + } -func newShCommand(ctx context.Context, c string) *exec.Cmd { - return exec.CommandContext(ctx, ShPath, "-c", c) + p, err := syntax.Parse(strings.NewReader(opts.Command), "", 0) + if err != nil { + return err + } + + r := interp.Runner{ + Context: opts.Context, + File: p, + Dir: opts.Dir, + Env: opts.Env, + Stdin: opts.Stdin, + Stdout: opts.Stdout, + Stderr: opts.Stderr, + } + if err = r.Run(); err != nil { + return err + } + return nil } diff --git a/execext/exec_other.go b/execext/exec_other.go deleted file mode 100644 index 5f36715c..00000000 --- a/execext/exec_other.go +++ /dev/null @@ -1,14 +0,0 @@ -// +build !windows - -package execext - -import ( - "context" - "os/exec" -) - -// NewCommand returns a new command that runs on "sh" is available or on "cmd" -// otherwise on Windows -func NewCommand(ctx context.Context, c string) *exec.Cmd { - return newShCommand(ctx, c) -} diff --git a/execext/exec_win.go b/execext/exec_win.go deleted file mode 100644 index 64ec3194..00000000 --- a/execext/exec_win.go +++ /dev/null @@ -1,21 +0,0 @@ -// +build windows - -package execext - -import ( - "context" - "os/exec" -) - -// NewCommand returns a new command that runs on "sh" is available or on "cmd" -// otherwise on Windows -func NewCommand(ctx context.Context, c string) *exec.Cmd { - if ShExists { - return newShCommand(ctx, c) - } - return newCmdCommand(ctx, c) -} - -func newCmdCommand(ctx context.Context, c string) *exec.Cmd { - return exec.CommandContext(ctx, "cmd", "/C", c) -} diff --git a/task.go b/task.go index 5fae5c4e..a7298ded 100644 --- a/task.go +++ b/task.go @@ -1,6 +1,7 @@ package task import ( + "bytes" "context" "fmt" "log" @@ -177,38 +178,54 @@ func (t *Task) runCommand(ctx context.Context, i int) error { if err != nil { return err } - cmd := execext.NewCommand(ctx, c) - if dir != "" { - cmd.Dir = dir + + envs, err := t.getEnviron(vars) + if err != nil { + return err } - if t.Env != nil { - cmd.Env = os.Environ() - for key, value := range t.Env { - replacedValue, err := ReplaceVariables(value, vars) - if err != nil { - return err - } - replacedKey, err := ReplaceVariables(key, vars) - if err != nil { - return err - } - cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", replacedKey, replacedValue)) - } + opts := &execext.RunCommandOptions{ + Context: ctx, + Command: c, + Dir: dir, + Env: envs, + Stdin: os.Stdin, + Stderr: os.Stderr, } - cmd.Stdin = os.Stdin - cmd.Stderr = os.Stderr - if t.Set != "" { - bytes, err := cmd.Output() - if err != nil { + + if t.Set == "" { + log.Println(c) + opts.Stdout = os.Stdout + if err = execext.RunCommand(opts); err != nil { return err } - os.Setenv(t.Set, strings.TrimSpace(string(bytes))) - return nil - } - cmd.Stdout = os.Stdout - log.Println(c) - if err = cmd.Run(); err != nil { - return err + } else { + buff := bytes.NewBuffer(nil) + opts.Stdout = buff + if err = execext.RunCommand(opts); err != nil { + return err + } + os.Setenv(t.Set, strings.TrimSpace(buff.String())) } return nil } + +func (t *Task) getEnviron(vars map[string]string) ([]string, error) { + if t.Env == nil { + return nil, nil + } + + envs := os.Environ() + + for k, v := range t.Env { + replacedValue, err := ReplaceVariables(v, vars) + if err != nil { + return nil, err + } + replacedKey, err := ReplaceVariables(k, vars) + if err != nil { + return nil, err + } + envs = append(envs, fmt.Sprintf("%s=%s", replacedKey, replacedValue)) + } + return envs, nil +} diff --git a/variable_handling.go b/variable_handling.go index 1f4829bc..22f8f45d 100644 --- a/variable_handling.go +++ b/variable_handling.go @@ -2,7 +2,6 @@ package task import ( "bytes" - "context" "encoding/json" "errors" "io/ioutil" @@ -35,19 +34,25 @@ func handleDynamicVariableContent(value string) (string, error) { if result, ok := varCmds[value]; ok { return result, nil } - cmd := execext.NewCommand(context.Background(), value[1:]) - cmd.Stderr = os.Stderr - b, err := cmd.Output() - if err != nil { + + buff := bytes.NewBuffer(nil) + + opts := &execext.RunCommandOptions{ + Command: strings.TrimPrefix(value, "$"), + Stdout: buff, + Stderr: os.Stderr, + } + if err := execext.RunCommand(opts); err != nil { return "", err } - if b[len(b)-1] == '\n' { - b = b[:len(b)-1] - } - if bytes.ContainsRune(b, '\n') { + + result := buff.String() + result = strings.TrimSuffix(result, "\n") + if strings.ContainsRune(result, '\n') { return "", ErrMultilineResultCmd } - result := strings.TrimSpace(string(b)) + + result = strings.TrimSpace(result) varCmds[value] = result return result, nil } @@ -84,17 +89,12 @@ func init() { taskFuncs := template.FuncMap{ "OS": func() string { return runtime.GOOS }, "ARCH": func() string { return runtime.GOARCH }, - "IsSH": func() bool { return execext.ShExists }, + // historical reasons + "IsSH": func() bool { return true }, "FromSlash": func(path string) string { - if execext.ShExists { - return path - } return filepath.FromSlash(path) }, "ToSlash": func(path string) string { - if execext.ShExists { - return path - } return filepath.ToSlash(path) }, }