Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 10 additions & 91 deletions sh/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,10 @@
package sh

import (
"bytes"
"context"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"strings"

"github.com/magefile/mage/mg"
"github.com/magefile/mage/shctx"
)

// RunCmd returns a function that will call Run with the given command. This is
Expand Down Expand Up @@ -50,46 +43,35 @@ func OutCmd(cmd string, args ...string) func(args ...string) (string, error) {

// Run is like RunWith, but doesn't specify any environment variables.
func Run(cmd string, args ...string) error {
return RunWith(nil, cmd, args...)
return shctx.Run(context.Background(), cmd, args...)
}

// RunV is like Run, but always sends the command's stdout to os.Stdout.
func RunV(cmd string, args ...string) error {
_, err := Exec(nil, os.Stdout, os.Stderr, cmd, args...)
return err
return shctx.RunV(context.Background(), cmd, args...)
}

// RunWith runs the given command, directing stderr to this program's stderr and
// printing stdout to stdout if mage was run with -v. It adds adds env to the
// environment variables for the command being run. Environment variables should
// be in the format name=value.
func RunWith(env map[string]string, cmd string, args ...string) error {
var output io.Writer
if mg.Verbose() {
output = os.Stdout
}
_, err := Exec(env, output, os.Stderr, cmd, args...)
return err
return shctx.RunWith(context.Background(), env, cmd, args...)
}

// RunWithV is like RunWith, but always sends the command's stdout to os.Stdout.
func RunWithV(env map[string]string, cmd string, args ...string) error {
_, err := Exec(env, os.Stdout, os.Stderr, cmd, args...)
return err
return shctx.RunWithV(context.Background(), env, cmd, args...)
}

// Output runs the command and returns the text from stdout.
func Output(cmd string, args ...string) (string, error) {
buf := &bytes.Buffer{}
_, err := Exec(nil, buf, os.Stderr, cmd, args...)
return strings.TrimSuffix(buf.String(), "\n"), err
return shctx.Output(context.Background(), cmd, args...)
}

// OutputWith is like RunWith, but returns what is written to stdout.
func OutputWith(env map[string]string, cmd string, args ...string) (string, error) {
buf := &bytes.Buffer{}
_, err := Exec(env, buf, os.Stderr, cmd, args...)
return strings.TrimSuffix(buf.String(), "\n"), err
return shctx.OutputWith(context.Background(), env, cmd, args...)
}

// Exec executes the command, piping its stdout and stderr to the given
Expand All @@ -105,47 +87,7 @@ func OutputWith(env map[string]string, cmd string, args ...string) (string, erro
// Code reports the exit code the command returned if it ran. If err == nil, ran
// is always true and code is always 0.
func Exec(env map[string]string, stdout, stderr io.Writer, cmd string, args ...string) (ran bool, err error) {
expand := func(s string) string {
s2, ok := env[s]
if ok {
return s2
}
return os.Getenv(s)
}
cmd = os.Expand(cmd, expand)
for i := range args {
args[i] = os.Expand(args[i], expand)
}
ran, code, err := doRun(env, stdout, stderr, cmd, args...)
if err == nil {
return true, nil
}
if ran {
return ran, mg.Fatalf(code, `running "%s %s" failed with exit code %d`, cmd, strings.Join(args, " "), code)
}
return ran, fmt.Errorf(`failed to run "%s %s: %w"`, cmd, strings.Join(args, " "), err)
}

func doRun(env map[string]string, stdout, stderr io.Writer, cmd string, args ...string) (ran bool, code int, err error) {
c := exec.CommandContext(context.Background(), cmd, args...)
c.Env = os.Environ()
for k, v := range env {
c.Env = append(c.Env, k+"="+v)
}
c.Stderr = stderr
c.Stdout = stdout
c.Stdin = os.Stdin

var quoted []string
for i := range args {
quoted = append(quoted, fmt.Sprintf("%q", args[i]))
}
// To protect against logging from doing exec in global variables
if mg.Verbose() {
log.Println("exec:", cmd, strings.Join(quoted, " "))
}
err = c.Run()
return CmdRan(err), ExitStatus(err), err
return shctx.Exec(context.Background(), env, stdout, stderr, cmd, args...)
}

// CmdRan examines the error to determine if it was generated as a result of a
Expand All @@ -155,35 +97,12 @@ func doRun(env map[string]string, stdout, stderr io.Writer, cmd string, args ...
// the command failed to run (usually due to the command not existing or not
// being executable), it reports false.
func CmdRan(err error) bool {
if err == nil {
return true
}
var ee *exec.ExitError
if errors.As(err, &ee) {
return ee.Exited()
}
return false
}

type exitStatus interface {
ExitStatus() int
return shctx.CmdRan(err)
}

// ExitStatus returns the exit status of the error if it is an exec.ExitError
// or if it implements ExitStatus() int.
// 0 if it is nil or 1 if it is a different error.
func ExitStatus(err error) int {
if err == nil {
return 0
}
if e, ok := err.(exitStatus); ok {
return e.ExitStatus()
}
var e *exec.ExitError
if errors.As(err, &e) {
if ex, ok := e.Sys().(exitStatus); ok {
return ex.ExitStatus()
}
}
return 1
return shctx.ExitStatus(err)
}
124 changes: 0 additions & 124 deletions sh/cmd_test.go

This file was deleted.

33 changes: 3 additions & 30 deletions sh/helpers.go
Original file line number Diff line number Diff line change
@@ -1,43 +1,16 @@
package sh

import (
"fmt"
"io"
"os"
"github.com/magefile/mage/shctx"
)

// Rm removes the given file or directory even if non-empty. It will not return
// an error if the target doesn't exist, only if the target cannot be removed.
func Rm(path string) error {
err := os.RemoveAll(path)
if err == nil || os.IsNotExist(err) {
return nil
}
return fmt.Errorf(`failed to remove %s: %w`, path, err)
return shctx.Rm(path)
}

// Copy robustly copies the source file to the destination, overwriting the destination if necessary.
func Copy(dst, src string) error {
from, err := os.Open(src)
if err != nil {
return fmt.Errorf(`can't copy %s: %w`, src, err)
}
defer func() { _ = from.Close() }()
finfo, err := from.Stat()
if err != nil {
return fmt.Errorf(`can't stat %s: %w`, src, err)
}
to, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, finfo.Mode())
if err != nil {
return fmt.Errorf(`can't copy to %s: %w`, dst, err)
}
_, err = io.Copy(to, from)
if err != nil {
_ = to.Close()
return fmt.Errorf(`error copying %s to %s: %w`, src, dst, err)
}
if err := to.Close(); err != nil {
return fmt.Errorf(`error closing %s: %w`, dst, err)
}
return nil
return shctx.Copy(dst, src)
}
Loading