Skip to content

Commit 9091329

Browse files
authored
Merge pull request #483 from anuraaga/process-args
support commands specified as exe/args
2 parents 60c0883 + 7b6523b commit 9091329

File tree

4 files changed

+61
-1
lines changed

4 files changed

+61
-1
lines changed

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/google/go-cmp v0.6.0
99
github.com/mattn/go-colorable v0.1.13
1010
github.com/mattn/go-runewidth v0.0.16
11+
github.com/mattn/go-shellwords v1.0.12
1112
github.com/robfig/cron/v3 v3.0.1
1213
github.com/yuin/goldmark v1.7.8
1314
golang.org/x/sync v0.9.0

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
1111
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
1212
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
1313
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
14+
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
15+
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
1416
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
1517
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
1618
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=

process.go

+27-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os/exec"
88
"sync"
99

10+
"github.com/mattn/go-shellwords"
1011
"golang.org/x/sync/errgroup"
1112
"golang.org/x/sync/semaphore"
1213
"golang.org/x/sys/execabs"
@@ -109,18 +110,36 @@ func (proc *concurrentProcess) wait() {
109110
// newCommandRunner creates new external command runner for given executable. The executable path
110111
// is resolved in this function.
111112
func (proc *concurrentProcess) newCommandRunner(exe string, combineOutput bool) (*externalCommand, error) {
112-
p, err := execabs.LookPath(exe)
113+
var args []string
114+
p, args, err := findExe(exe)
113115
if err != nil {
114116
return nil, err
115117
}
116118
cmd := &externalCommand{
117119
proc: proc,
118120
exe: p,
121+
args: args,
119122
combineOutput: combineOutput,
120123
}
121124
return cmd, nil
122125
}
123126

127+
func findExe(exe string) (string, []string, error) {
128+
p, err := execabs.LookPath(exe)
129+
if err == nil {
130+
return p, nil, nil
131+
}
132+
// See if the command string contains args. As it is best effort, we do not
133+
// handle parse errors.
134+
if exeArgs, _ := shellwords.Parse(exe); len(exeArgs) > 0 {
135+
if p, err := execabs.LookPath(exeArgs[0]); err == nil {
136+
return p, exeArgs[1:], nil
137+
}
138+
}
139+
140+
return "", nil, err
141+
}
142+
124143
// externalCommand is struct to run specific command concurrently with concurrentProcess bounding
125144
// number of processes at the same time. This type manages fatal errors while running the command
126145
// by using errgroup.Group. The wait() method must be called at the end for checking if some fatal
@@ -129,13 +148,20 @@ type externalCommand struct {
129148
proc *concurrentProcess
130149
eg errgroup.Group
131150
exe string
151+
args []string
132152
combineOutput bool
133153
}
134154

135155
// run runs the command with given arguments and stdin. The callback function is called after the
136156
// process runs. First argument is stdout and the second argument is an error while running the
137157
// process.
138158
func (cmd *externalCommand) run(args []string, stdin string, callback func([]byte, error) error) {
159+
if len(cmd.args) > 0 {
160+
var allArgs []string
161+
allArgs = append(allArgs, cmd.args...)
162+
allArgs = append(allArgs, args...)
163+
args = allArgs
164+
}
139165
exec := &cmdExecution{cmd.exe, args, stdin, cmd.combineOutput}
140166
cmd.proc.run(&cmd.eg, exec, callback)
141167
}

process_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"sync/atomic" // Note: atomic.Bool was added at Go 1.19
88
"testing"
99
"time"
10+
11+
"golang.org/x/sys/execabs"
1012
)
1113

1214
func testStartEchoCommand(t *testing.T, proc *concurrentProcess, done *atomic.Bool) {
@@ -63,6 +65,35 @@ func TestProcessRunConcurrently(t *testing.T) {
6365
}
6466
}
6567

68+
func TestProcessRunWithArgs(t *testing.T) {
69+
if _, err := execabs.LookPath("echo"); err != nil {
70+
t.Skipf("echo command is necessary to run this test: %s", err)
71+
}
72+
73+
var done atomic.Bool
74+
p := newConcurrentProcess(1)
75+
echo, err := p.newCommandRunner("echo hello", false)
76+
if err != nil {
77+
t.Fatalf(`parsing "echo hello" failed: %v`, err)
78+
}
79+
echo.run(nil, "", func(b []byte, err error) error {
80+
if err != nil {
81+
t.Error(err)
82+
return err
83+
}
84+
if string(b) != "hello\n" {
85+
t.Errorf("unexpected output: %q", b)
86+
}
87+
done.Store(true)
88+
return nil
89+
})
90+
p.wait()
91+
92+
if !done.Load() {
93+
t.Error("callback did not run")
94+
}
95+
}
96+
6697
func TestProcessRunMultipleCommandsConcurrently(t *testing.T) {
6798
p := newConcurrentProcess(3)
6899

0 commit comments

Comments
 (0)