Description
Describe the feature
This is a proposal to simplify the execution of multiple commands, where an error should stop this chain. Basically the behaviour of make
when executing multiple commands for a single target.
What problem does this feature address?
AFAIK this is the way to execute multiple commands in a "build" target:
func Build() error {
if err := sh.Run("go", "mod", "download"); err != nil {
return err
}
if err := sh.Run("go", "build", "./cmd/gcoderamp"); err != nil {
return err
}
return sh.Run("go", "build", "./cmd/pcb2gcodewrap")
}
This means that for every command to execute there are typically three lines of code necessary. My proposal is to introduce something like this:
func Build(ctx context.Context) error {
r := NewRunner(ctx)
r.Run("go", "mod", "download")
r.Run("go", "build", "./cmd/gcoderamp")
r.Run("go", "build", "./cmd/pcb2gcodewrap")
return r.Wait()
}
I think it's a good idea to make running shell commands as direct as possible.
Additional context
This is the way I implemented it locally. It does add the dependency on golang.org/x/sync/errgroup
, so the code cannot be used as-is. I think it's relatively simple to restructure the code to avoid this dependency. Before doing that, I'd rather discuss the overall idea first, though.
//go:build mage
package main
import (
"context"
"github.com/magefile/mage/sh"
"golang.org/x/sync/errgroup"
)
// Runner allows running a group of commands sequentially, stopping at the first
// failure.
type Runner struct {
group *errgroup.Group
ctx context.Context
}
// NewRunner constructs a new runner that's bound to the given context. If the
// context is done, no new command will be executed. It does NOT abort an
// already-running command.
func NewRunner(ctx context.Context) *Runner {
group, groupctx := errgroup.WithContext(ctx)
group.SetLimit(1)
return &Runner{
group: group,
ctx: groupctx,
}
}
// Run the given command.
// This only runs a command if no previous command has failed yet.
func (r *Runner) Run(cmd string, args ...string) {
r.group.Go(func() error {
if err := r.ctx.Err(); err != nil {
return err
}
return sh.Run(cmd, args...)
})
}
// Wait for the commands to finish running, and return any error.
func (r *Runner) Wait() error {
return r.group.Wait()
}