Skip to content

Commit fa4c557

Browse files
Merge pull request #75 from go-cmd/dn/v1.4
Rework Cmd.Options as Options.BeforeExec
2 parents 6480229 + 36d7189 commit fa4c557

File tree

4 files changed

+110
-32
lines changed

4 files changed

+110
-32
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# go-cmd/cmd Changelog
22

3+
## v1.4
4+
5+
### v1.4.0 (2022-01-01)
6+
7+
* Added `Options.BeforeExec` based on PR #53 #54 by @wenerme (issue #53)
8+
39
## v1.3
410

511
### v1.3.1 (2021-10-13)

cmd.go

+69-26
Original file line numberDiff line numberDiff line change
@@ -53,37 +53,55 @@ import (
5353
"time"
5454
)
5555

56-
// Apply ExecCmdOption before process start, used for customize SysProcAttr.
57-
type ExecCmdOption func(cmd *exec.Cmd)
58-
5956
// Cmd represents an external command, similar to the Go built-in os/exec.Cmd.
6057
// A Cmd cannot be reused after calling Start. Exported fields are read-only and
6158
// should not be modified, except Env which can be set before calling Start.
6259
// To create a new Cmd, call NewCmd or NewCmdOptions.
6360
type Cmd struct {
64-
Name string
65-
Args []string
66-
Env []string
67-
Dir string
68-
Options []ExecCmdOption
69-
Stdout chan string // streaming STDOUT if enabled, else nil (see Options)
70-
Stderr chan string // streaming STDERR if enabled, else nil (see Options)
61+
// Name of binary (command) to run. This is the only required value.
62+
// No path expansion is done.
63+
// Used to set underlying os/exec.Cmd.Path.
64+
Name string
65+
66+
// Commands line arguments passed to the command.
67+
// Args are optional.
68+
// Used to set underlying os/exec.Cmd.Args.
69+
Args []string
70+
71+
// Environment variables set before running the command.
72+
// Env is optional.
73+
Env []string
74+
75+
// Current working directory from which to run the command.
76+
// Dir is optional; default is current working directory
77+
// of calling process.
78+
// Used to set underlying os/exec.Cmd.Dir.
79+
Dir string
80+
81+
// Stdout sets streaming STDOUT if enabled, else nil (see Options).
82+
Stdout chan string
83+
84+
// Stderr sets streaming STDERR if enabled, else nil (see Options).
85+
Stderr chan string
86+
7187
*sync.Mutex
72-
started bool // cmd.Start called, no error
73-
stopped bool // Stop called
74-
done bool // run() done
75-
final bool // status finalized in Status
76-
startTime time.Time // if started true
77-
stdoutBuf *OutputBuffer
78-
stderrBuf *OutputBuffer
79-
stdoutStream *OutputStream
80-
stderrStream *OutputStream
81-
status Status
82-
statusChan chan Status // nil until Start() called
83-
doneChan chan struct{} // closed when done running
88+
started bool // cmd.Start called, no error
89+
stopped bool // Stop called
90+
done bool // run() done
91+
final bool // status finalized in Status
92+
startTime time.Time // if started true
93+
stdoutBuf *OutputBuffer
94+
stderrBuf *OutputBuffer
95+
stdoutStream *OutputStream
96+
stderrStream *OutputStream
97+
status Status
98+
statusChan chan Status // nil until Start() called
99+
doneChan chan struct{} // closed when done running
100+
beforeExecFuncs []func(cmd *exec.Cmd)
84101
}
85102

86103
var (
104+
// ErrNotStarted is returned by Stop if called before Start or StartWithStdin.
87105
ErrNotStarted = errors.New("command not running")
88106
)
89107

@@ -134,14 +152,20 @@ type Options struct {
134152
// faster and more efficient than polling Cmd.Status. The caller must read both
135153
// streaming channels, else lines are dropped silently.
136154
Streaming bool
155+
156+
// BeforeExec is a list of functions called immediately before starting
157+
// the real command. These functions can be used to customize the underlying
158+
// os/exec.Cmd. For example, to set SysProcAttr.
159+
BeforeExec []func(cmd *exec.Cmd)
137160
}
138161

139162
// NewCmdOptions creates a new Cmd with options. The command is not started
140163
// until Start is called.
141164
func NewCmdOptions(options Options, name string, args ...string) *Cmd {
142165
c := &Cmd{
143-
Name: name,
144-
Args: args,
166+
Name: name,
167+
Args: args,
168+
// --
145169
Mutex: &sync.Mutex{},
146170
status: Status{
147171
Cmd: name,
@@ -167,6 +191,16 @@ func NewCmdOptions(options Options, name string, args ...string) *Cmd {
167191
c.stderrStream = NewOutputStream(c.Stderr)
168192
}
169193

194+
if len(options.BeforeExec) > 0 {
195+
c.beforeExecFuncs = []func(cmd *exec.Cmd){}
196+
for _, f := range options.BeforeExec {
197+
if f == nil {
198+
continue
199+
}
200+
c.beforeExecFuncs = append(c.beforeExecFuncs, f)
201+
}
202+
}
203+
170204
return c
171205
}
172206

@@ -185,7 +219,14 @@ func (c *Cmd) Clone() *Cmd {
185219
)
186220
clone.Dir = c.Dir
187221
clone.Env = c.Env
188-
clone.Options = c.Options
222+
223+
if len(c.beforeExecFuncs) > 0 {
224+
clone.beforeExecFuncs = make([]func(cmd *exec.Cmd), len(c.beforeExecFuncs))
225+
for i := range c.beforeExecFuncs {
226+
clone.beforeExecFuncs[i] = c.beforeExecFuncs[i]
227+
}
228+
}
229+
189230
return clone
190231
}
191232

@@ -374,7 +415,9 @@ func (c *Cmd) run(in io.Reader) {
374415
// is nil, use the current process' environment.
375416
cmd.Env = c.Env
376417
cmd.Dir = c.Dir
377-
for _, f := range c.Options {
418+
419+
// Run all optional commands to customize underlying os/exe.Cmd.
420+
for _, f := range c.beforeExecFuncs {
378421
f(cmd)
379422
}
380423

cmd_test.go

+34-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//go:build !windows
12
// +build !windows
23

34
package cmd_test
@@ -1128,14 +1129,42 @@ func TestStdinOk(t *testing.T) {
11281129
}
11291130
}
11301131

1131-
func TestExecCCmdOptions(t *testing.T) {
1132-
p := cmd.NewCmd("/bin/ls")
1132+
func TestOptionsBeforeExec(t *testing.T) {
11331133
handled := false
1134-
p.Options = append(p.Options, func(cmd *exec.Cmd) {
1135-
handled = true
1136-
})
1134+
p := cmd.NewCmdOptions(
1135+
cmd.Options{
1136+
BeforeExec: []func(cmd *exec.Cmd){
1137+
func(cmd *exec.Cmd) { handled = true },
1138+
},
1139+
},
1140+
"/bin/ls",
1141+
)
1142+
<-p.Start()
1143+
if !handled {
1144+
t.Error("exec cmd option not applied")
1145+
}
1146+
1147+
// nil funcs should be ignored, not cause a panic
1148+
handled = false
1149+
p = cmd.NewCmdOptions(
1150+
cmd.Options{
1151+
BeforeExec: []func(cmd *exec.Cmd){
1152+
nil,
1153+
func(cmd *exec.Cmd) { handled = true },
1154+
},
1155+
},
1156+
"/bin/ls",
1157+
)
11371158
<-p.Start()
11381159
if !handled {
11391160
t.Error("exec cmd option not applied")
11401161
}
1162+
1163+
// Cloning should copy the funcs
1164+
handled = false
1165+
p2 := p.Clone()
1166+
<-p2.Start()
1167+
if !handled {
1168+
t.Error("exec cmd option not applied")
1169+
}
11411170
}

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module github.com/go-cmd/cmd
22

3-
go 1.16
3+
go 1.17
44

55
require github.com/go-test/deep v1.0.7

0 commit comments

Comments
 (0)