123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141 |
- 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
- }
- }
|