Skip to content

Commit a58807a

Browse files
adding some utilities for scripting (cadence-workflow#6958)
1 parent ae5840b commit a58807a

File tree

2 files changed

+222
-0
lines changed

2 files changed

+222
-0
lines changed

common/scripting/exec.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package scripting
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"errors"
7+
"io"
8+
"os"
9+
"os/exec"
10+
)
11+
12+
// Executor is a helper for running scripting
13+
type Executor interface {
14+
BashExec(ctx context.Context, in string) (stdout string, stderr string, exitErr *exec.ExitError)
15+
Exec(ctx context.Context, bin string, args ...string) (stdout string, stderr string, exitErr *exec.ExitError)
16+
QuietBashExec(ctx context.Context, in string) (stdout string, stderr string, exitErr *exec.ExitError)
17+
QuietExec(ctx context.Context, bin string, args ...string) (stdout string, stderr string, exitErr *exec.ExitError)
18+
}
19+
20+
// New returns a new bash executor
21+
func New() Executor {
22+
return &execImpl{}
23+
}
24+
25+
type execImpl struct{}
26+
27+
// BashExec is a helper function for ensuring the user
28+
// can read a legible streaming from a subprocess,
29+
// as well as allowing programmatic access to stdout, stderr and exit codes
30+
// it's highly unsafe for any kind of untrusted inputs as it's explicitly bypassing
31+
// go's exec safety args, so it *must not* come into contact with anything untrusted
32+
func (execImpl) BashExec(ctx context.Context, in string) (stdout string, stderr string, exitErr *exec.ExitError) {
33+
cmd := exec.CommandContext(ctx, "bash", "-c", in)
34+
var stdBuffer bytes.Buffer
35+
var stdErrBuffer bytes.Buffer
36+
mw := io.MultiWriter(os.Stdout, &stdBuffer)
37+
mwErr := io.MultiWriter(os.Stderr, &stdErrBuffer)
38+
cmd.Stdout = mw
39+
cmd.Stderr = mwErr
40+
err := cmd.Run()
41+
var e *exec.ExitError
42+
if errors.As(err, &e) && e.ExitCode() != 0 {
43+
return stdBuffer.String(), stdErrBuffer.String(), e
44+
} else if err != nil {
45+
panic(err)
46+
}
47+
return stdBuffer.String(), stdErrBuffer.String(), nil
48+
}
49+
50+
// Exec is a wrapper around exec.Command which adds some convenience
51+
// functionality to both capture standout/err as well as tee it to the user's UI in real time
52+
// meaning that the user doesn't need to wait for the command to complete.
53+
// It's value is fairly marginal and if it presents any problems the user should consider just
54+
// using exec.Command directly
55+
func (execImpl) Exec(ctx context.Context, bin string, args ...string) (stdout string, stderr string, exitErr *exec.ExitError) {
56+
cmd := exec.CommandContext(ctx, bin, args...)
57+
var stdBuffer bytes.Buffer
58+
var stdErrBuffer bytes.Buffer
59+
mw := io.MultiWriter(os.Stdout, &stdBuffer)
60+
mwErr := io.MultiWriter(os.Stderr, &stdErrBuffer)
61+
cmd.Stdout = mw
62+
cmd.Stderr = mwErr
63+
err := cmd.Run()
64+
var e *exec.ExitError
65+
if errors.As(err, &e) && e.ExitCode() != 0 {
66+
return stdBuffer.String(), stdErrBuffer.String(), e
67+
} else if err != nil {
68+
panic(err)
69+
}
70+
return stdBuffer.String(), stdErrBuffer.String(), nil
71+
}
72+
73+
// QuietBashExec ...
74+
func (execImpl) QuietBashExec(ctx context.Context, in string) (stdout string, stderr string, exitErr *exec.ExitError) {
75+
cmd := exec.CommandContext(ctx, "bash", "-c", in)
76+
var stdBuffer bytes.Buffer
77+
var stdErrBuffer bytes.Buffer
78+
cmd.Stdout = &stdBuffer
79+
cmd.Stderr = &stdErrBuffer
80+
err := cmd.Run()
81+
var e *exec.ExitError
82+
if errors.As(err, &e) && e.ExitCode() != 0 {
83+
return stdBuffer.String(), stdErrBuffer.String(), e
84+
} else if err != nil {
85+
panic(err)
86+
}
87+
return stdBuffer.String(), stdErrBuffer.String(), nil
88+
}
89+
90+
// QuietExec ...
91+
func (execImpl) QuietExec(ctx context.Context, bin string, args ...string) (stdout string, stderr string, exitErr *exec.ExitError) {
92+
cmd := exec.CommandContext(ctx, bin, args...)
93+
var stdBuffer bytes.Buffer
94+
var stdErrBuffer bytes.Buffer
95+
cmd.Stdout = &stdBuffer
96+
cmd.Stderr = &stdErrBuffer
97+
err := cmd.Run()
98+
var e *exec.ExitError
99+
if errors.As(err, &e) && e.ExitCode() != 0 {
100+
return stdBuffer.String(), stdErrBuffer.String(), e
101+
} else if err != nil {
102+
panic(err)
103+
}
104+
return stdBuffer.String(), stdErrBuffer.String(), nil
105+
}

common/scripting/exec_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package scripting
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestBashExec(t *testing.T) {
11+
script := New()
12+
13+
stdout, stderr, err := script.BashExec(context.Background(), "echo test")
14+
assert.Equal(t, "test\n", stdout)
15+
assert.Equal(t, "", stderr)
16+
assert.Nil(t, err)
17+
18+
stdout, stderr, err = script.BashExec(context.Background(), "echo test 1>&2")
19+
assert.Equal(t, "test\n", stderr)
20+
assert.Equal(t, "", stdout)
21+
assert.Nil(t, err)
22+
23+
stdout, stderr, err = script.BashExec(context.Background(), "false")
24+
assert.Equal(t, "", stderr)
25+
assert.Equal(t, "", stdout)
26+
assert.Error(t, err)
27+
28+
tests := map[string]struct {
29+
args string
30+
expectedStdout string
31+
expectedSterr string
32+
expectedErr bool
33+
}{
34+
"stdout": {
35+
args: "echo test",
36+
expectedStdout: "test\n",
37+
expectedSterr: "",
38+
expectedErr: false,
39+
},
40+
"stderr": {
41+
args: "echo test 1>&2",
42+
expectedStdout: "",
43+
expectedSterr: "test\n",
44+
expectedErr: false,
45+
},
46+
"error": {
47+
args: "false",
48+
expectedStdout: "",
49+
expectedSterr: "",
50+
expectedErr: true,
51+
},
52+
}
53+
54+
for name, td := range tests {
55+
t.Run(name, func(t *testing.T) {
56+
stdout, stderr, err := script.BashExec(context.Background(), td.args)
57+
assert.Equal(t, td.expectedStdout, stdout)
58+
assert.Equal(t, td.expectedSterr, stderr)
59+
if td.expectedErr {
60+
assert.Error(t, err)
61+
}
62+
63+
stdout, stderr, err = script.QuietBashExec(context.Background(), td.args)
64+
assert.Equal(t, td.expectedStdout, stdout)
65+
assert.Equal(t, td.expectedSterr, stderr)
66+
if td.expectedErr {
67+
assert.Error(t, err)
68+
}
69+
})
70+
}
71+
72+
}
73+
74+
func TestExec(t *testing.T) {
75+
script := New()
76+
77+
tests := map[string]struct {
78+
bin string
79+
args []string
80+
expectedStdout string
81+
expectedSterr string
82+
expectedErr bool
83+
}{
84+
"stdout": {
85+
bin: "echo",
86+
args: []string{"test"},
87+
expectedStdout: "test\n",
88+
expectedSterr: "",
89+
expectedErr: false,
90+
},
91+
"error": {
92+
bin: "false",
93+
args: []string{""},
94+
expectedStdout: "",
95+
expectedSterr: "",
96+
expectedErr: true,
97+
},
98+
}
99+
100+
for name, td := range tests {
101+
t.Run(name, func(t *testing.T) {
102+
stdout, stderr, err := script.Exec(context.Background(), td.bin, td.args...)
103+
assert.Equal(t, td.expectedStdout, stdout)
104+
assert.Equal(t, td.expectedSterr, stderr)
105+
if td.expectedErr {
106+
assert.Error(t, err)
107+
}
108+
109+
stdout, stderr, err = script.QuietExec(context.Background(), td.bin, td.args...)
110+
assert.Equal(t, td.expectedStdout, stdout)
111+
assert.Equal(t, td.expectedSterr, stderr)
112+
if td.expectedErr {
113+
assert.Error(t, err)
114+
}
115+
})
116+
}
117+
}

0 commit comments

Comments
 (0)