Repository: magefile/mage (go)

Issue:
Support passthrough stdin
If a command run by mage requires user input, mage should allow the user to enter that input.

Pull request description:
passthrough stdin
This causes mage to pass through its stdin to running processes and
causes the sh commands to do the same, so that commands that expect
input from the user can get it.  Fixes #62.

Patch:
diff --git a/mage/main.go b/mage/main.go
index 1f97e7c..30444bb 100644
--- a/mage/main.go
+++ b/mage/main.go
@@ -47,7 +47,7 @@ var (
 // function to allow it to be used from other programs, specifically so you can
 // go run a simple file that run's mage's Main.
 func Main() int {
-	return ParseAndRun(".", os.Stdout, os.Stderr, os.Args[1:])
+	return ParseAndRun(".", os.Stdout, os.Stderr, os.Stdin, os.Args[1:])
 }
 
 // Invocation contains the args for invoking a run of Mage.
@@ -60,17 +60,19 @@ type Invocation struct {
 	Keep    bool      // tells mage to keep the generated main file after compiling
 	Stdout  io.Writer // writer to write stdout messages to
 	Stderr  io.Writer // writer to write stderr messages to
+	Stdin   io.Reader // reader to read stdin from
 	Args    []string  // args to pass to the compiled binary
 }
 
 // ParseAndRun parses the command line, and then compiles and runs the mage
 // files in the given directory with the given args (do not include the command
 // name in the args).
-func ParseAndRun(dir string, stdout, stderr io.Writer, args []string) int {
+func ParseAndRun(dir string, stdout, stderr io.Writer, stdin io.Reader, args []string) int {
 	log := log.New(stderr, "", 0)
 	inv, mageInit, showVersion, err := Parse(stdout, args)
 	inv.Dir = dir
 	inv.Stderr = stderr
+	inv.Stdin = stdin
 	if err == flag.ErrHelp {
 		return 0
 	}
@@ -147,7 +149,6 @@ func Parse(stdout io.Writer, args []string) (inv Invocation, mageInit, showVersi
 // Invoke runs Mage with the given arguments.
 func Invoke(inv Invocation) int {
 	log := log.New(inv.Stderr, "", 0)
-
 	if len(inv.Args) > 1 {
 		log.Printf("Error: args after the target (%s) are not allowed: %v", inv.Args[0], strings.Join(inv.Args[1:], " "))
 		return 2
@@ -349,6 +350,7 @@ func RunCompiled(inv Invocation, exePath string) int {
 	c := exec.Command(exePath, inv.Args...)
 	c.Stderr = inv.Stderr
 	c.Stdout = inv.Stdout
+	c.Stdin = inv.Stdin
 	c.Env = os.Environ()
 	if inv.Verbose {
 		c.Env = append(c.Env, "MAGEFILE_VERBOSE=1")
diff --git a/sh/cmd.go b/sh/cmd.go
index 9c5c880..23fc372 100644
--- a/sh/cmd.go
+++ b/sh/cmd.go
@@ -50,6 +50,12 @@ func Run(cmd string, args ...string) error {
 	return RunWith(nil, 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
+}
+
 // 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
@@ -119,6 +125,7 @@ func run(env map[string]string, stdout, stderr io.Writer, cmd string, args ...st
 	}
 	c.Stderr = stderr
 	c.Stdout = stdout
+	c.Stdin = os.Stdin
 	log.Println("exec:", cmd, strings.Join(args, " "))
 	err = c.Run()
 	return cmdRan(err), ExitStatus(err), err


Test patch:
diff --git a/mage/main_test.go b/mage/main_test.go
index 180f75b..00b95e8 100644
--- a/mage/main_test.go
+++ b/mage/main_test.go
@@ -9,6 +9,7 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"strings"
 	"testing"
 
 	"github.com/magefile/mage/mg"
@@ -119,6 +120,7 @@ func TestList(t *testing.T) {
 	actual := stdout.String()
 	expected := `
 Targets:
+  copyStdin             
   panics                Function that panics.
   panicsErr             Error function that panics.
   returnsError*         Synopsis for returns error.
@@ -178,6 +180,27 @@ func TestTargetError(t *testing.T) {
 	}
 }
 
+func TestStdinCopy(t *testing.T) {
+	stdout := &bytes.Buffer{}
+	stdin := strings.NewReader("hi!")
+	inv := Invocation{
+		Dir:    "./testdata",
+		Stderr: ioutil.Discard,
+		Stdout: stdout,
+		Stdin:  stdin,
+		Args:   []string{"CopyStdin"},
+	}
+	code := Invoke(inv)
+	if code != 0 {
+		t.Fatalf("expected 0, but got %v", code)
+	}
+	actual := stdout.String()
+	expected := "hi!"
+	if actual != expected {
+		t.Fatalf("expected %q, but got %q", expected, actual)
+	}
+}
+
 func TestTargetPanics(t *testing.T) {
 	stderr := &bytes.Buffer{}
 	inv := Invocation{
diff --git a/mage/testdata/func.go b/mage/testdata/func.go
index 7e8f386..651cfe0 100644
--- a/mage/testdata/func.go
+++ b/mage/testdata/func.go
@@ -4,6 +4,8 @@ package main
 
 import (
 	"fmt"
+	"io"
+	"os"
 )
 
 // Synopsis for returns error.
@@ -13,4 +15,9 @@ func ReturnsError() error {
 	return nil
 }
 
+func CopyStdin() error {
+	_, err := io.Copy(os.Stdout, os.Stdin)
+	return err
+}
+
 func nonexported() {}