Skip to content

Enhancement: Simpler way to execute multiple commands #455

Open
@sybrenstuvel

Description

@sybrenstuvel

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()
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions