From 7c0f49e80710be4595dc61261db09e129a98e8ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 21:23:53 +0000 Subject: [PATCH 1/2] Add context-aware shell helper package (fork of mage/sh) Copy all exported functions from github.com/magefile/mage/sh and add context.Context as the first parameter to every command-running function. The context is passed through to exec.CommandContext for cancellation and timeout support. Remove the dependency on github.com/magefile/mage/mg by inlining the verbose check and fatal error type. Agent-Logs-Url: https://github.com/magefile/shx/sessions/f0fda061-9670-4a20-a708-2ab74aa13234 Co-authored-by: natefinch <3185864+natefinch@users.noreply.github.com> --- README.md | 71 ++++++++++++++- cmd.go | 220 +++++++++++++++++++++++++++++++++++++++++++++++ cmd_test.go | 143 ++++++++++++++++++++++++++++++ go.mod | 3 + helpers.go | 43 +++++++++ helpers_test.go | 177 ++++++++++++++++++++++++++++++++++++++ testmain_test.go | 46 ++++++++++ 7 files changed, 702 insertions(+), 1 deletion(-) create mode 100644 cmd.go create mode 100644 cmd_test.go create mode 100644 go.mod create mode 100644 helpers.go create mode 100644 helpers_test.go create mode 100644 testmain_test.go diff --git a/README.md b/README.md index 08b8ea4..7d25f32 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,71 @@ # shx -A shell-like command runner for go, compatible with mage. + +A context-aware shell helper package for Go, compatible with [mage](https://magefile.org). + +`shx` is a fork of [`github.com/magefile/mage/sh`](https://pkg.go.dev/github.com/magefile/mage/sh) that adds a `context.Context` parameter to every exported function that runs a command. The context is passed through to `exec.CommandContext`, giving callers control over timeouts, cancellation, and deadlines. + +## Installation + +```bash +go get github.com/magefile/shx +``` + +## Usage + +```go +package main + +import ( + "context" + "log" + "time" + + "github.com/magefile/shx" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Run a command (output goes to stdout when MAGEFILE_VERBOSE=1) + if err := shx.Run(ctx, "go", "build", "./..."); err != nil { + log.Fatal(err) + } + + // Capture output + out, err := shx.Output(ctx, "git", "rev-parse", "HEAD") + if err != nil { + log.Fatal(err) + } + log.Println("HEAD:", out) + + // Create reusable command aliases + goTest := shx.RunCmd("go", "test") + if err := goTest(ctx, "-v", "./..."); err != nil { + log.Fatal(err) + } +} +``` + +## API + +All command-running functions from `mage/sh` are available with the same names. The only difference is that each accepts a `context.Context` as its first parameter: + +| Function | Description | +|---|---| +| `Run(ctx, cmd, args...)` | Run a command | +| `RunV(ctx, cmd, args...)` | Run a command, always printing stdout | +| `RunWith(ctx, env, cmd, args...)` | Run with extra environment variables | +| `RunWithV(ctx, env, cmd, args...)` | RunWith, always printing stdout | +| `Output(ctx, cmd, args...)` | Run and capture stdout | +| `OutputWith(ctx, env, cmd, args...)` | Output with extra environment variables | +| `Exec(ctx, env, stdout, stderr, cmd, args...)` | Full control over I/O | +| `RunCmd(cmd, args...)` | Returns a reusable `func(ctx, args...) error` | +| `OutCmd(cmd, args...)` | Returns a reusable `func(ctx, args...) (string, error)` | + +File helpers that don't execute commands are unchanged: + +| Function | Description | +|---|---| +| `Copy(dst, src)` | Copy a file | +| `Rm(path)` | Remove a file or directory | diff --git a/cmd.go b/cmd.go new file mode 100644 index 0000000..b482b16 --- /dev/null +++ b/cmd.go @@ -0,0 +1,220 @@ +// Package shx provides helpers for running shell commands with context support. +// +// It is a context-aware fork of github.com/magefile/mage/sh. Every exported +// function that executes a command accepts a [context.Context] as its first +// parameter and passes it through to [exec.CommandContext]. +package shx + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "log" + "os" + "os/exec" + "strconv" + "strings" +) + +// VerboseEnv is the environment variable that indicates the user requested +// verbose mode when running a magefile. +const VerboseEnv = "MAGEFILE_VERBOSE" + +func verbose() bool { + b, _ := strconv.ParseBool(os.Getenv(VerboseEnv)) + return b +} + +// RunCmd returns a function that will call Run with the given command. This is +// useful for creating command aliases to make your scripts easier to read, like +// this: +// +// // in a helper file somewhere +// var g0 = shx.RunCmd("go") // go is a keyword :( +// +// // somewhere in your main code +// if err := g0(ctx, "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 = shx.RunCmd("go", "install") +// goInstall(ctx, "github.com/gohugo/hugo") +// +// RunCmd uses Exec underneath, so see those docs for more details. +func RunCmd(cmd string, args ...string) func(ctx context.Context, args ...string) error { + return func(ctx context.Context, args2 ...string) error { + return Run(ctx, cmd, append(args, args2...)...) + } +} + +// OutCmd is like RunCmd except the command returns the output of the +// command. +func OutCmd(cmd string, args ...string) func(ctx context.Context, args ...string) (string, error) { + return func(ctx context.Context, args2 ...string) (string, error) { + return Output(ctx, cmd, append(args, args2...)...) + } +} + +// Run is like RunWith, but doesn't specify any environment variables. +func Run(ctx context.Context, cmd string, args ...string) error { + return RunWith(ctx, nil, cmd, args...) +} + +// RunV is like Run, but always sends the command's stdout to os.Stdout. +func RunV(ctx context.Context, cmd string, args ...string) error { + _, err := Exec(ctx, nil, os.Stdout, os.Stderr, cmd, args...) + return err +} + +// 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 env to the +// environment variables for the command being run. Environment variables should +// be in the format name=value. +func RunWith(ctx context.Context, env map[string]string, cmd string, args ...string) error { + var output io.Writer + if verbose() { + output = os.Stdout + } + _, err := Exec(ctx, env, output, os.Stderr, cmd, args...) + return err +} + +// RunWithV is like RunWith, but always sends the command's stdout to os.Stdout. +func RunWithV(ctx context.Context, env map[string]string, cmd string, args ...string) error { + _, err := Exec(ctx, env, os.Stdout, os.Stderr, cmd, args...) + return err +} + +// Output runs the command and returns the text from stdout. +func Output(ctx context.Context, cmd string, args ...string) (string, error) { + buf := &bytes.Buffer{} + _, err := Exec(ctx, nil, buf, os.Stderr, cmd, args...) + return strings.TrimSuffix(buf.String(), "\n"), err +} + +// OutputWith is like RunWith, but returns what is written to stdout. +func OutputWith(ctx context.Context, env map[string]string, cmd string, args ...string) (string, error) { + buf := &bytes.Buffer{} + _, err := Exec(ctx, env, buf, os.Stderr, cmd, args...) + return strings.TrimSuffix(buf.String(), "\n"), err +} + +// Exec 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 +// running the command, these override the current environment variables set +// (which are also passed to the command). cmd and args may include references +// to environment variables in $FOO format, in which case these will be +// expanded before the command is run. +// +// 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(ctx context.Context, 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(ctx, env, stdout, stderr, cmd, args...) + if err == nil { + return true, nil + } + if ran { + return ran, 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(ctx context.Context, env map[string]string, stdout, stderr io.Writer, cmd string, args ...string) (ran bool, code int, err error) { + c := exec.CommandContext(ctx, 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 verbose() { + log.Println("exec:", cmd, strings.Join(quoted, " ")) + } + 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 +// error is an unrecognized type, or it is an error from exec.Command that says +// 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 +} + +// 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 +} + +// fatalError is an error with an associated exit code, compatible with +// mage's mg.Fatalf return type. +type fatalError struct { + error + code int +} + +func (f fatalError) ExitStatus() int { + return f.code +} + +func fatalf(code int, format string, args ...interface{}) error { + return fatalError{ + code: code, + error: fmt.Errorf(format, args...), + } +} diff --git a/cmd_test.go b/cmd_test.go new file mode 100644 index 0000000..12519fe --- /dev/null +++ b/cmd_test.go @@ -0,0 +1,143 @@ +package shx + +import ( + "bytes" + "context" + "errors" + "os" + "testing" +) + +func TestOutCmd(t *testing.T) { + ctx := context.Background() + cmd := OutCmd(os.Args[0], "-printArgs", "foo", "bar") + out, err := cmd(ctx, "baz", "bat") + if err != nil { + t.Fatal(err) + } + expected := "[foo bar baz bat]" + if out != expected { + t.Fatalf("expected %q but got %q", expected, out) + } +} + +func TestExitCode(t *testing.T) { + ctx := context.Background() + ran, err := Exec(ctx, nil, nil, nil, os.Args[0], "-helper", "-exit", "99") + if err == nil { + t.Fatal("unexpected nil error from run") + } + if !ran { + t.Error("ran returned as false, but should have been true") + } + code := ExitStatus(err) + if code != 99 { + t.Fatalf("expected exit status 99, but got %v", code) + } +} + +func TestEnv(t *testing.T) { + ctx := context.Background() + env := "SOME_REALLY_LONG_MAGEFILE_SPECIFIC_THING" + out := &bytes.Buffer{} + ran, err := Exec(ctx, map[string]string{env: "foobar"}, out, nil, os.Args[0], "-printVar", env) + 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() != "foobar\n" { + t.Errorf("expected foobar, got %q", out) + } +} + +func TestNotRun(t *testing.T) { + ctx := context.Background() + ran, err := Exec(ctx, nil, nil, nil, "thiswontwork") + if err == nil { + t.Fatal("unexpected nil error") + } + if ran { + t.Fatal("expected ran to be false but was true") + } +} + +func TestAutoExpand(t *testing.T) { + ctx := context.Background() + t.Setenv("MAGE_FOOBAR", "baz") + s, err := Output(ctx, "echo", "$MAGE_FOOBAR") + if err != nil { + t.Fatal(err) + } + if s != "baz" { + t.Fatalf(`Expected "baz" but got %q`, s) + } +} + +func TestCmdRanNilErr(t *testing.T) { + if !CmdRan(nil) { + t.Fatal("CmdRan(nil) should return true") + } +} + +func TestCmdRanNotFound(t *testing.T) { + ctx := context.Background() + _, err := Exec(ctx, nil, nil, nil, "thiswontwork") + if CmdRan(err) { + t.Fatal("CmdRan should return false for not-found command") + } +} + +func TestExitStatusNil(t *testing.T) { + code := ExitStatus(nil) + if code != 0 { + t.Fatalf("expected 0 for nil error, got %d", code) + } +} + +func TestExitStatusNonExecError(t *testing.T) { + code := ExitStatus(errors.New("generic error")) + if code != 1 { + t.Fatalf("expected 1 for generic error, got %d", code) + } +} + +func TestExitStatusFromExec(t *testing.T) { + ctx := context.Background() + _, err := Exec(ctx, nil, nil, nil, os.Args[0], "-helper", "-exit", "42") + code := ExitStatus(err) + if code != 42 { + t.Fatalf("expected exit status 42, got %d", code) + } +} + +func TestRunCmd(t *testing.T) { + ctx := context.Background() + echoHello := RunCmd("echo", "hello") + err := echoHello(ctx, "world") + // RunWith directs output based on verbose, so just check no error + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestOutputWith(t *testing.T) { + ctx := context.Background() + out, err := OutputWith(ctx, map[string]string{"MY_TEST_VAR": "xyz"}, os.Args[0], "-printVar", "MY_TEST_VAR") + if err != nil { + t.Fatal(err) + } + if out != "xyz" { + t.Fatalf("expected 'xyz', got %q", out) + } +} + +func TestContextCancellation(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() // cancel immediately + _, err := Exec(ctx, nil, nil, nil, "sleep", "10") + if err == nil { + t.Fatal("expected error from cancelled context") + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..17c31ef --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/magefile/shx + +go 1.24.13 diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..815469a --- /dev/null +++ b/helpers.go @@ -0,0 +1,43 @@ +package shx + +import ( + "fmt" + "io" + "os" +) + +// 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) +} + +// 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 +} diff --git a/helpers_test.go b/helpers_test.go new file mode 100644 index 0000000..0fa792c --- /dev/null +++ b/helpers_test.go @@ -0,0 +1,177 @@ +package shx_test + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/magefile/shx" +) + +// compareFiles checks that two files are identical for testing purposes. That means they have the same length, +// the same contents, and the same permissions. It does NOT mean they have the same timestamp, as that is expected +// to change in normal shx.Copy operation. +func compareFiles(file1, file2 string) error { + s1, err := os.Stat(file1) + if err != nil { + return fmt.Errorf("can't stat %s: %w", file1, err) + } + s2, err := os.Stat(file2) + if err != nil { + return fmt.Errorf("can't stat %s: %w", file2, err) + } + if s1.Size() != s2.Size() { + return fmt.Errorf("files %s and %s have different sizes: %d vs %d", file1, file2, s1.Size(), s2.Size()) + } + if s1.Mode() != s2.Mode() { + return fmt.Errorf("files %s and %s have different permissions: %#4o vs %#4o", file1, file2, s1.Mode(), s2.Mode()) + } + f1bytes, err := os.ReadFile(file1) + if err != nil { + return fmt.Errorf("can't read %s: %w", file1, err) + } + f2bytes, err := os.ReadFile(file2) + if err != nil { + return fmt.Errorf("can't read %s: %w", file2, err) + } + if !bytes.Equal(f1bytes, f2bytes) { + return fmt.Errorf("files %s and %s have different contents", file1, file2) + } + return nil +} + +func TestHelpers(t *testing.T) { + mytmpdir := t.TempDir() + defer func() { + derr := os.RemoveAll(mytmpdir) + if derr != nil { + fmt.Printf("error cleaning up after TestHelpers: %v", derr) + } + }() + srcname := filepath.Join(mytmpdir, "test1.txt") + err := os.WriteFile(srcname, []byte("All work and no play makes Jack a dull boy."), 0o600) + if err != nil { + t.Fatalf("can't create test file %s: %v", srcname, err) + } + destname := filepath.Join(mytmpdir, "test2.txt") + + t.Run("shx/copy", func(t *testing.T) { + cerr := shx.Copy(destname, srcname) + if cerr != nil { + t.Errorf("test file copy from %s to %s failed: %v", srcname, destname, cerr) + } + cerr = compareFiles(srcname, destname) + if cerr != nil { + t.Errorf("test file copy verification failed: %v", cerr) + } + }) + + // While we've got a temporary directory, test how forgiving shx.Rm is + t.Run("shx/rm/ne", func(t *testing.T) { + nef := filepath.Join(mytmpdir, "file_not_exist.txt") + rerr := shx.Rm(nef) + if rerr != nil { + t.Errorf("shx.Rm complained when removing nonexistent file %s: %v", nef, rerr) + } + }) + + t.Run("shx/copy/ne", func(t *testing.T) { + nef := filepath.Join(mytmpdir, "file_not_exist.txt") + nedf := filepath.Join(mytmpdir, "file_not_exist2.txt") + cerr := shx.Copy(nedf, nef) + if cerr == nil { + t.Errorf("shx.Copy succeeded copying nonexistent file %s", nef) + } + }) + + // We test shx.Rm by clearing up our own test files and directories + t.Run("shx/rm", func(t *testing.T) { + rerr := shx.Rm(destname) + if rerr != nil { + t.Errorf("failed to remove file %s: %v", destname, rerr) + } + rerr = shx.Rm(srcname) + if rerr != nil { + t.Errorf("failed to remove file %s: %v", srcname, rerr) + } + rerr = shx.Rm(mytmpdir) + if rerr != nil { + t.Errorf("failed to remove dir %s: %v", mytmpdir, rerr) + } + _, rerr = os.Stat(mytmpdir) + if rerr == nil { + t.Errorf("removed dir %s but it's still there?", mytmpdir) + } + }) + + t.Run("shx/rm/nedir", func(t *testing.T) { + rerr := shx.Rm(mytmpdir) + if rerr != nil { + t.Errorf("shx.Rm complained removing nonexistent dir %s", mytmpdir) + } + }) +} + +func TestCopyNonExistentSource(t *testing.T) { + dir := t.TempDir() + dst := filepath.Join(dir, "dst.txt") + err := shx.Copy(dst, filepath.Join(dir, "nonexistent.txt")) + if err == nil { + t.Fatal("expected error copying from non-existent source") + } +} + +func TestCopyReadOnlyDir(t *testing.T) { + dir := t.TempDir() + src := filepath.Join(dir, "src.txt") + if err := os.WriteFile(src, []byte("data"), 0o600); err != nil { + t.Fatal(err) + } + // Try to copy to a path inside a non-existent subdirectory + dst := filepath.Join(dir, "nodir", "dst.txt") + err := shx.Copy(dst, src) + if err == nil { + t.Fatal("expected error copying to non-existent directory") + } +} + +func TestCopyPreservesPermissions(t *testing.T) { + dir := t.TempDir() + src := filepath.Join(dir, "src.txt") + if err := os.WriteFile(src, []byte("data"), 0o600); err != nil { + t.Fatal(err) + } + dst := filepath.Join(dir, "dst.txt") + if err := shx.Copy(dst, src); err != nil { + t.Fatal(err) + } + srcInfo, _ := os.Stat(src) + dstInfo, _ := os.Stat(dst) + if srcInfo.Mode() != dstInfo.Mode() { + t.Errorf("permissions differ: src=%v dst=%v", srcInfo.Mode(), dstInfo.Mode()) + } +} + +func TestCopyOverwrite(t *testing.T) { + dir := t.TempDir() + src := filepath.Join(dir, "src.txt") + dst := filepath.Join(dir, "dst.txt") + if err := os.WriteFile(src, []byte("new content"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(dst, []byte("old content"), 0o600); err != nil { + t.Fatal(err) + } + if err := shx.Copy(dst, src); err != nil { + t.Fatal(err) + } + data, err := os.ReadFile(dst) + if err != nil { + t.Fatal(err) + } + if string(data) != "new content" { + t.Fatalf("expected 'new content', got %q", string(data)) + } +} diff --git a/testmain_test.go b/testmain_test.go new file mode 100644 index 0000000..da74964 --- /dev/null +++ b/testmain_test.go @@ -0,0 +1,46 @@ +package shx + +import ( + "flag" + "fmt" + "os" + "testing" +) + +var ( + helperCmd bool + printArgs bool + stderr string + stdout string + exitCode int + printVar string +) + +func init() { //nolint:gochecknoinits // required for test flag setup + flag.BoolVar(&helperCmd, "helper", false, "") + flag.BoolVar(&printArgs, "printArgs", false, "") + flag.StringVar(&stderr, "stderr", "", "") + flag.StringVar(&stdout, "stdout", "", "") + flag.IntVar(&exitCode, "exit", 0, "") + flag.StringVar(&printVar, "printVar", "", "") +} + +func TestMain(m *testing.M) { + flag.Parse() + + if printArgs { + fmt.Println(flag.Args()) + return + } + if printVar != "" { + fmt.Println(os.Getenv(printVar)) + return + } + + if helperCmd { + _, _ = fmt.Fprintln(os.Stderr, stderr) + _, _ = fmt.Fprintln(os.Stdout, stdout) + os.Exit(exitCode) + } + os.Exit(m.Run()) +} From 7c55a82b2f33c0215c117250218faef5e511036e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 21:24:47 +0000 Subject: [PATCH 2/2] Fix doc comments to remove mage/mg references Agent-Logs-Url: https://github.com/magefile/shx/sessions/f0fda061-9670-4a20-a708-2ab74aa13234 Co-authored-by: natefinch <3185864+natefinch@users.noreply.github.com> --- cmd.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cmd.go b/cmd.go index b482b16..47dc208 100644 --- a/cmd.go +++ b/cmd.go @@ -73,7 +73,7 @@ func RunV(ctx context.Context, cmd string, args ...string) error { } // 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 env to the +// printing stdout to os.Stdout if MAGEFILE_VERBOSE is set. It adds env to the // environment variables for the command being run. Environment variables should // be in the format name=value. func RunWith(ctx context.Context, env map[string]string, cmd string, args ...string) error { @@ -106,9 +106,8 @@ func OutputWith(ctx context.Context, env map[string]string, cmd string, args ... } // Exec 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 +// writers. If the command fails, it will return an error that contains the +// exit code of the failed command. Env is a list of environment variables to set when // running the command, these override the current environment variables set // (which are also passed to the command). cmd and args may include references // to environment variables in $FOO format, in which case these will be