-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcommand.go
214 lines (191 loc) · 5.22 KB
/
command.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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
package main
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"sort"
"strings"
)
// stderr can be changed in tests to capture output of Cmd's methods.
var stderr io.Writer = os.Stderr
// Verbose can be used to make all Cmds run with forced LogAlways.
var Verbose = false
// NewEnviron returns a clone of the original array of KEY=VALUE entries, with
// entries from the patch array merged (overriding existing KEYs).
//
// FIXME(mateuszc): test what happens for multiline values in real env
func NewEnviron(original []string, patch ...string) []string {
// Parse the entries present in 'patch'
mpatch := map[string]string{}
for _, entry := range patch {
split := strings.SplitN(entry, "=", 2)
if len(split) != 2 {
panic(fmt.Sprintf("unexpected environ patch entry: %q", entry))
}
mpatch[split[0]] = split[1]
}
// Copy only the entries not present in 'patch'
result := []string{}
for _, entry := range original {
split := strings.SplitN(entry, "=", 2)
if len(split) != 2 {
panic(fmt.Sprintf("unexpected environ original entry: %q", entry))
}
_, found := mpatch[split[0]]
if !found {
result = append(result, entry)
}
}
// Add entries from 'patch'
result = append(result, patch...)
return result
}
type LogMode int
const (
// LogOnError is the default logging mode. If a Cmd method returns error,
// the command-line is printed to os.Stderr, together with full command
// output (stderr and stdout), and any changed environment variables.
//
// Example resulting output:
//
// # GOPATH= go list -- net/http foobar
// can't load package: package foobar: cannot find package "foobar" in any of:
// /usr/local/go/src/foobar (from $GOROOT)
// ($GOPATH not set)
// net/http
LogOnError LogMode = iota
// LogAlways always prints the command-line to os.Stderr. The command
// output (stderr and stdout) is however only printed in case of error.
LogAlways
// LogNever never prints anything to os.Stderr.
LogNever
)
type Cmd struct {
Cmd *exec.Cmd
LogMode
}
func Command(command string, args ...string) *Cmd {
return &Cmd{
Cmd: exec.Command(command, args...),
LogMode: LogOnError,
}
}
// Append extends the cmd's argument list with args.
func (cmd *Cmd) Append(args ...string) *Cmd {
cmd.Cmd.Args = append(cmd.Cmd.Args, args...)
return cmd
}
// Setenv sets specified environment variables when running cmd. Each variable
// must be formatted as: "key=value".
func (cmd *Cmd) Setenv(variables ...string) *Cmd {
if cmd.Cmd.Env == nil {
cmd.Cmd.Env = os.Environ()
}
cmd.Cmd.Env = NewEnviron(cmd.Cmd.Env, variables...)
return cmd
}
// LogAlways changes what is printed to os.Stderr (see const LogAlways).
func (cmd *Cmd) LogAlways() *Cmd {
cmd.LogMode = LogAlways
return cmd
}
// LogNever changes what is printed to os.Stderr (see const LogNever).
func (cmd *Cmd) LogNever() *Cmd {
cmd.LogMode = LogNever
return cmd
}
// LogOnError changes what is printed to os.Stderr (see const LogOnError).
func (cmd *Cmd) LogOnError() *Cmd {
cmd.LogMode = LogOnError
return cmd
}
func (cmd *Cmd) CombinedOutput() ([]byte, error) {
if cmd.LogMode == LogAlways || Verbose {
cmd.printCmdWithEnv()
}
out, err := cmd.Cmd.CombinedOutput()
if err != nil {
if cmd.LogMode == LogOnError {
cmd.printCmdWithEnv()
}
if cmd.LogMode != LogNever || Verbose {
stderr.Write(out)
}
return nil, err
}
return out, nil
}
// OutputLines runs the command and returns trimmed stdout+stderr output split
// into lines.
func (cmd *Cmd) OutputLines() ([]string, error) {
out, err := cmd.CombinedOutput()
if err != nil {
return nil, err
}
out = bytes.TrimSpace(out)
if len(out) == 0 {
// Cannot leave this case to strings.Split(), as it would give us []string{""}
return nil, nil
}
lines := strings.Split(string(out), "\n")
return lines, nil
}
// OutputOneLine runs the command, verifies that exactly one line was printed to
// stdout+stderr, and returns it.
func (cmd *Cmd) OutputOneLine() (string, error) {
lines, err := cmd.OutputLines()
if err != nil {
return "", err
}
if len(lines) != 1 {
if cmd.LogMode == LogOnError {
cmd.printCmdWithEnv()
}
if cmd.LogMode != LogNever {
fmt.Fprintln(stderr, strings.Join(lines, "\n"))
}
return "", fmt.Errorf("expected one line of output from %s, got %d", cmd.Cmd.Args[0], len(lines))
}
return lines[0], nil
}
func (cmd *Cmd) DiscardOutput() error {
_, err := cmd.OutputLines()
return err
}
func envToMap(env []string) map[string]string {
m := map[string]string{}
for _, entry := range env {
key := strings.SplitN(entry, "=", 2)[0]
m[key] = entry
// TODO(mateuszc): if entries repeat, we override with later. Is that ok?
}
return m
}
func (cmd *Cmd) printCmdWithEnv() {
// Detect tweaks of environment variables.
// Note: this won't show changes done using os.Setenv()
diff := []string{}
if cmd.Cmd.Env != nil {
original := envToMap(os.Environ())
changed := envToMap(cmd.Cmd.Env)
for k, v := range changed {
if original[k] != v {
diff = append(diff, v+" ")
}
}
for k := range original {
_, found := changed[k]
if !found {
diff = append(diff, k+"= ")
}
}
}
sort.Strings(diff)
// Example output:
// # GOOS=windows GOARCH=amd64 go build .
fmt.Fprintf(stderr, "# %s%s\n",
strings.Join(diff, ""),
strings.Join(cmd.Cmd.Args, " "))
}