Skip to content

Commit

Permalink
Merge pull request #483 from anuraaga/process-args
Browse files Browse the repository at this point in the history
support commands specified as exe/args
  • Loading branch information
rhysd authored Nov 27, 2024
2 parents 60c0883 + 7b6523b commit 9091329
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 1 deletion.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/google/go-cmp v0.6.0
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-runewidth v0.0.16
github.com/mattn/go-shellwords v1.0.12
github.com/robfig/cron/v3 v3.0.1
github.com/yuin/goldmark v1.7.8
golang.org/x/sync v0.9.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
Expand Down
28 changes: 27 additions & 1 deletion process.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os/exec"
"sync"

"github.com/mattn/go-shellwords"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
"golang.org/x/sys/execabs"
Expand Down Expand Up @@ -109,18 +110,36 @@ func (proc *concurrentProcess) wait() {
// newCommandRunner creates new external command runner for given executable. The executable path
// is resolved in this function.
func (proc *concurrentProcess) newCommandRunner(exe string, combineOutput bool) (*externalCommand, error) {
p, err := execabs.LookPath(exe)
var args []string
p, args, err := findExe(exe)
if err != nil {
return nil, err
}
cmd := &externalCommand{
proc: proc,
exe: p,
args: args,
combineOutput: combineOutput,
}
return cmd, nil
}

func findExe(exe string) (string, []string, error) {
p, err := execabs.LookPath(exe)
if err == nil {
return p, nil, nil
}
// See if the command string contains args. As it is best effort, we do not
// handle parse errors.
if exeArgs, _ := shellwords.Parse(exe); len(exeArgs) > 0 {
if p, err := execabs.LookPath(exeArgs[0]); err == nil {
return p, exeArgs[1:], nil
}
}

return "", nil, err
}

// externalCommand is struct to run specific command concurrently with concurrentProcess bounding
// number of processes at the same time. This type manages fatal errors while running the command
// by using errgroup.Group. The wait() method must be called at the end for checking if some fatal
Expand All @@ -129,13 +148,20 @@ type externalCommand struct {
proc *concurrentProcess
eg errgroup.Group
exe string
args []string
combineOutput bool
}

// run runs the command with given arguments and stdin. The callback function is called after the
// process runs. First argument is stdout and the second argument is an error while running the
// process.
func (cmd *externalCommand) run(args []string, stdin string, callback func([]byte, error) error) {
if len(cmd.args) > 0 {
var allArgs []string
allArgs = append(allArgs, cmd.args...)
allArgs = append(allArgs, args...)
args = allArgs
}
exec := &cmdExecution{cmd.exe, args, stdin, cmd.combineOutput}
cmd.proc.run(&cmd.eg, exec, callback)
}
Expand Down
31 changes: 31 additions & 0 deletions process_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"sync/atomic" // Note: atomic.Bool was added at Go 1.19
"testing"
"time"

"golang.org/x/sys/execabs"
)

func testStartEchoCommand(t *testing.T, proc *concurrentProcess, done *atomic.Bool) {
Expand Down Expand Up @@ -63,6 +65,35 @@ func TestProcessRunConcurrently(t *testing.T) {
}
}

func TestProcessRunWithArgs(t *testing.T) {
if _, err := execabs.LookPath("echo"); err != nil {
t.Skipf("echo command is necessary to run this test: %s", err)
}

var done atomic.Bool
p := newConcurrentProcess(1)
echo, err := p.newCommandRunner("echo hello", false)
if err != nil {
t.Fatalf(`parsing "echo hello" failed: %v`, err)
}
echo.run(nil, "", func(b []byte, err error) error {
if err != nil {
t.Error(err)
return err
}
if string(b) != "hello\n" {
t.Errorf("unexpected output: %q", b)
}
done.Store(true)
return nil
})
p.wait()

if !done.Load() {
t.Error("callback did not run")
}
}

func TestProcessRunMultipleCommandsConcurrently(t *testing.T) {
p := newConcurrentProcess(3)

Expand Down

0 comments on commit 9091329

Please sign in to comment.