mirror of
https://github.com/go-task/task.git
synced 2024-12-12 10:45:49 +02:00
b259edeb65
Co-authored-by: Dor Sahar <dorsahar@icloud.com>
142 lines
3.2 KiB
Go
142 lines
3.2 KiB
Go
package execext
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"mvdan.cc/sh/v3/expand"
|
|
"mvdan.cc/sh/v3/interp"
|
|
"mvdan.cc/sh/v3/shell"
|
|
"mvdan.cc/sh/v3/syntax"
|
|
)
|
|
|
|
// RunCommandOptions is the options for the RunCommand func
|
|
type RunCommandOptions struct {
|
|
Command string
|
|
Dir string
|
|
Env []string
|
|
PosixOpts []string
|
|
BashOpts []string
|
|
Stdin io.Reader
|
|
Stdout io.Writer
|
|
Stderr io.Writer
|
|
}
|
|
|
|
// ErrNilOptions is returned when a nil options is given
|
|
var ErrNilOptions = errors.New("execext: nil options given")
|
|
|
|
// RunCommand runs a shell command
|
|
func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
|
|
if opts == nil {
|
|
return ErrNilOptions
|
|
}
|
|
|
|
// Set "-e" or "errexit" by default
|
|
opts.PosixOpts = append(opts.PosixOpts, "e")
|
|
|
|
// Format POSIX options into a slice that mvdan/sh understands
|
|
var params []string
|
|
for _, opt := range opts.PosixOpts {
|
|
if len(opt) == 1 {
|
|
params = append(params, fmt.Sprintf("-%s", opt))
|
|
} else {
|
|
params = append(params, "-o")
|
|
params = append(params, opt)
|
|
}
|
|
}
|
|
|
|
environ := opts.Env
|
|
if len(environ) == 0 {
|
|
environ = os.Environ()
|
|
}
|
|
|
|
r, err := interp.New(
|
|
interp.Params(params...),
|
|
interp.Env(expand.ListEnviron(environ...)),
|
|
interp.ExecHandlers(execHandler),
|
|
interp.OpenHandler(openHandler),
|
|
interp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr),
|
|
dirOption(opts.Dir),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
parser := syntax.NewParser()
|
|
|
|
// Run any shopt commands
|
|
if len(opts.BashOpts) > 0 {
|
|
shoptCmdStr := fmt.Sprintf("shopt -s %s", strings.Join(opts.BashOpts, " "))
|
|
shoptCmd, err := parser.Parse(strings.NewReader(shoptCmdStr), "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := r.Run(ctx, shoptCmd); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Run the user-defined command
|
|
p, err := parser.Parse(strings.NewReader(opts.Command), "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return r.Run(ctx, p)
|
|
}
|
|
|
|
// Expand is a helper to mvdan.cc/shell.Fields that returns the first field
|
|
// if available.
|
|
func Expand(s string) (string, error) {
|
|
s = filepath.ToSlash(s)
|
|
s = strings.ReplaceAll(s, " ", `\ `)
|
|
s = strings.ReplaceAll(s, "&", `\&`)
|
|
s = strings.ReplaceAll(s, "(", `\(`)
|
|
s = strings.ReplaceAll(s, ")", `\)`)
|
|
fields, err := shell.Fields(s, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(fields) > 0 {
|
|
return fields[0], nil
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func execHandler(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
|
|
return interp.DefaultExecHandler(15 * time.Second)
|
|
}
|
|
|
|
func openHandler(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {
|
|
if path == "/dev/null" {
|
|
return devNull{}, nil
|
|
}
|
|
return interp.DefaultOpenHandler()(ctx, path, flag, perm)
|
|
}
|
|
|
|
func dirOption(path string) interp.RunnerOption {
|
|
return func(r *interp.Runner) error {
|
|
err := interp.Dir(path)(r)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
// If the specified directory doesn't exist, it will be created later.
|
|
// Therefore, even if `interp.Dir` method returns an error, the
|
|
// directory path should be set only when the directory cannot be found.
|
|
if absPath, _ := filepath.Abs(path); absPath != "" {
|
|
if _, err := os.Stat(absPath); os.IsNotExist(err) {
|
|
r.Dir = absPath
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
}
|