-
Notifications
You must be signed in to change notification settings - Fork 6
/
exec.go
141 lines (123 loc) · 3.21 KB
/
exec.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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
}
}