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
112 changes: 93 additions & 19 deletions sh/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@ import (
// useful for creating command aliases to make your scripts easier to read, like
// this:
//
// // in a helper file somewhere
// var g0 = sh.RunCmd("go") // go is a keyword :(
// // in a helper file somewhere
// var g0 = sh.RunCmd("go") // go is a keyword :(
//
// // somewhere in your main code
// if err := g0("install", "github.com/gohugo/hugo"); err != nil {
// return err
// }
// // somewhere in your main code
// if err := g0("install", "github.com/gohugo/hugo"); err != nil {
// return err
// }
//
// Args passed to command get baked in as args to the command when you run it.
// Any args passed in when you run the returned function will be appended to the
// original args. For example, this is equivalent to the above:
//
// var goInstall = sh.RunCmd("go", "install") goInstall("github.com/gohugo/hugo")
// var goInstall = sh.RunCmd("go", "install") goInstall("github.com/gohugo/hugo")
//
// RunCmd uses Exec underneath, so see those docs for more details.
func RunCmd(cmd string, args ...string) func(args ...string) error {
Expand All @@ -37,6 +37,31 @@ func RunCmd(cmd string, args ...string) func(args ...string) error {
}
}

// RunAtCmd returns a function that will call Run with the given command at a given path.
// This is useful for creating command aliases to make your scripts easier to read, like
// this:
//
// // in a helper file somewhere
// var g0 = sh.RunAtCmd("go") // go is a keyword :(
//
// // somewhere in your main code
// if err := g0("/tmp", "install", "github.com/gohugo/hugo"); err != nil {
// return err
// }
//
// Args passed to command get baked in as args to the command when you run it.
// Any args passed in when you run the returned function will be appended to the
// original args. For example, this is equivalent to the above:
//
// var goInstall = sh.RunAtCmd("go", "install") goInstall("tmp", "github.com/gohugo/hugo")
//
// RunAtCmd uses Exec underneath, so see those docs for more details.
func RunAtCmd(cmd string, args ...string) func(pwd string, args ...string) error {
return func(pwd string, args2 ...string) error {
return RunAt(pwd, cmd, append(args, args2...)...)
}
}

// OutCmd is like RunCmd except the command returns the output of the
// command.
func OutCmd(cmd string, args ...string) func(args ...string) (string, error) {
Expand All @@ -45,14 +70,32 @@ func OutCmd(cmd string, args ...string) func(args ...string) (string, error) {
}
}

// OutAtCmd is like RunAtCmd except the command returns the output of the
// command.
func OutAtCmd(cmd string, args ...string) func(pwd string, args ...string) (string, error) {
return func(pwd string, args2 ...string) (string, error) {
return OutputAt(pwd, cmd, append(args, args2...)...)
}
}

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

// RunAt is like RunAtWith, but doesn't specify any environment variables.
func RunAt(pwd, cmd string, args ...string) error {
return RunAtWith(nil, pwd, 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 RunAtV("", cmd, args...)
}

// RunAtV is like RunAt, but always sends the command's stdout to os.Stdout.
func RunAtV(pwd string, cmd string, args ...string) error {
_, err := ExecAt(nil, os.Stdout, os.Stderr, pwd, cmd, args...)
return err
}

Expand All @@ -61,35 +104,63 @@ func RunV(cmd string, args ...string) error {
// 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 {
return RunAtWith(env, "", cmd, args...)
}

// RunAtWith runs the given command at a certain path, 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 RunAtWith(env map[string]string, pwd string, cmd string, args ...string) error {
var output io.Writer
if mg.Verbose() {
output = os.Stdout
}
_, err := Exec(env, output, os.Stderr, cmd, args...)
_, err := ExecAt(env, output, os.Stderr, pwd, cmd, args...)
return err
}

// 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 RunAtWithV(env, "", cmd, args...)
}

// RunAtWithV is like RunAtWith, but always sends the command's stdout to os.Stdout.
func RunAtWithV(env map[string]string, pwd string, cmd string, args ...string) error {
_, err := ExecAt(env, os.Stdout, os.Stderr, pwd, cmd, args...)
return err
}

// Output runs the command and returns the text from stdout.
func Output(cmd string, args ...string) (string, error) {
return OutputAt("", cmd, args...)
}

// OutputAt runs the command at a certain path and returns the text from stdout.
func OutputAt(pwd string, cmd string, args ...string) (string, error) {
buf := &bytes.Buffer{}
_, err := Exec(nil, buf, os.Stderr, cmd, args...)
_, err := ExecAt(nil, buf, os.Stderr, pwd, cmd, args...)
return strings.TrimSuffix(buf.String(), "\n"), err
}

// OutputWith is like RunWith, but returns what is written to stdout.
func OutputWith(env map[string]string, cmd string, args ...string) (string, error) {
return OutputAtWith(env, "", cmd, args...)
}

// OutputAt With is like RunAtWith, but returns what is written to stdout.
func OutputAtWith(env map[string]string, pwd string, cmd string, args ...string) (string, error) {
buf := &bytes.Buffer{}
_, err := Exec(env, buf, os.Stderr, cmd, args...)
_, err := ExecAt(env, buf, os.Stderr, pwd, cmd, args...)
return strings.TrimSuffix(buf.String(), "\n"), err
}

// Exec executes the command, piping its stdout and stderr to the given
// Exec is like execAt but always runs in the current workdir.
func Exec(env map[string]string, stdout, stderr io.Writer, cmd string, args ...string) (ran bool, err error) {
return ExecAt(env, stdout, stderr, "", cmd, args...)
}

// ExecAt executes the command, piping its stdout and stderr to the given
// writers. If the command fails, it will return an error that, if returned
// from a target or mg.Deps call, will cause mage to exit with the same code as
// the command failed with. Env is a list of environment variables to set when
Expand All @@ -101,7 +172,8 @@ func OutputWith(env map[string]string, cmd string, args ...string) (string, erro
// Ran reports if the command ran (rather than was not found or not executable).
// 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) {

func ExecAt(env map[string]string, stdout, stderr io.Writer, pwd string, cmd string, args ...string) (ran bool, err error) {
expand := func(s string) string {
s2, ok := env[s]
if ok {
Expand All @@ -113,7 +185,7 @@ func Exec(env map[string]string, stdout, stderr io.Writer, cmd string, args ...s
for i := range args {
args[i] = os.Expand(args[i], expand)
}
ran, code, err := run(env, stdout, stderr, cmd, args...)
ran, code, err := run(env, stdout, stderr, pwd, cmd, args...)
if err == nil {
return true, nil
}
Expand All @@ -123,27 +195,29 @@ func Exec(env map[string]string, stdout, stderr io.Writer, cmd string, args ...s
return ran, fmt.Errorf(`failed to run "%s %s: %v"`, cmd, strings.Join(args, " "), err)
}

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

var quoted []string
var quoted []string
for i := range args {
quoted = append(quoted, fmt.Sprintf("%q", args[i]));
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, " "))
log.Printf("exec: %s %s, pwd: %s\n", cmd, strings.Join(quoted, " "), pwd)
}
err = c.Run()
return CmdRan(err), ExitStatus(err), err
}

// CmdRan examines the error to determine if it was generated as a result of a
// command running via os/exec.Command. If the error is nil, or the command ran
// (even if it exited with a non-zero exit code), CmdRan reports true. If the
Expand Down
54 changes: 54 additions & 0 deletions sh/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sh

import (
"bytes"
"fmt"
"os"
"testing"
)
Expand All @@ -18,6 +19,18 @@ func TestOutCmd(t *testing.T) {
}
}

func TestOutAtCmd(t *testing.T) {
cmd := OutAtCmd("sh", "-c")
out, err := cmd("/", "pwd")
if err != nil {
t.Fatal(err)
}
expected := "/"
if out != expected {
t.Fatalf("expected %q but got %q", expected, out)
}
}

func TestExitCode(t *testing.T) {
ran, err := Exec(nil, nil, nil, os.Args[0], "-helper", "-exit", "99")
if err == nil {
Expand Down Expand Up @@ -68,5 +81,46 @@ func TestAutoExpand(t *testing.T) {
if s != "baz" {
t.Fatalf(`Expected "baz" but got %q`, s)
}
}

func TestSettingPwd(t *testing.T) {
pwd := "/"
out := &bytes.Buffer{}
ran, err := ExecAt(nil, out, nil, pwd, "pwd")
if err != nil {
t.Fatalf("unexpected error from runner: %#v", err)
}
if !ran {
t.Error("expected ran to be true but was false.")
}
if out.String() != fmt.Sprintf("%s\n", pwd) {
t.Errorf("expected %s, got %q", fmt.Sprintf("%s\n", pwd), out)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is fmt.Sprintf("%s\n", pwd) used here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because we are running the command "pwd" which should result in the correct pwd, which is set to be "/". So that is what we expect to get in return.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use t.Errorf("expected %s, got %q", pwd, out) or t.Errorf("expected %s\n, got %q", pwd, out) (if a newline is needed)?

}
}

func TestSettingNoPwd(t *testing.T) {
currentWd, err := os.Getwd()
if err != nil {
t.Errorf("Failed getting current working directory: %v", err)
}
out := &bytes.Buffer{}
ran, err := ExecAt(nil, out, nil, "", "pwd")
if err != nil {
t.Fatalf("unexpected error from runner: %#v", err)
}
if !ran {
t.Errorf("expected ran to be true but was false.")
}
if out.String() != fmt.Sprintf("%s\n", currentWd) {
t.Errorf("expected %s, got %q", fmt.Sprintf("%s\n", currentWd), out)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is fmt.Sprintf("%s\n", currentWd) used?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we run ExecAt with pwd "", we should test that this returns the actuall current path.

}
}

func TestSettingInvalidPwd(t *testing.T) {
pwd := "/i-am-expected-to-not-exist"
out := &bytes.Buffer{}
_, err := ExecAt(nil, out, nil, pwd, "pwd")
if err == nil {
t.Fatalf("Expected error because path %s does not exist", pwd)
}
}