Repository: go-task/task
Pull request #1344 (2023-09-21 23:14:02)

feat: unify prompts
This allows the `--yes` flag to work for all prompts (including the ones in the remote Taskfiles experiment) and reduces some duplicated prompt code.


wburningham: @pd93 would you be willing to merge this and tag a new `v3.30.2` version? (I'm at the point where I'd like to test the CI integration within my company)

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8978ea55..119adac8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
 
 ## Unreleased
 
+- Enabled the `--yes` flag for the
+  [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles)
+  (#1344 by @pd93).
 - Add ability to set `watch: true` in a task to automatically run it in watch
   mode (#231, #1361 by @andreynering).
 - Fixed a bug on the watch mode where paths that contained `.git` (like
diff --git a/docs/docs/experiments/remote_taskfiles.md b/docs/docs/experiments/remote_taskfiles.md
index d95d8dab..56ecdc81 100644
--- a/docs/docs/experiments/remote_taskfiles.md
+++ b/docs/docs/experiments/remote_taskfiles.md
@@ -54,6 +54,16 @@ Taskfiles:
    code `104` (not trusted) and nothing will run. If you accept the prompt, the
    checksum will be updated and the remote Taskfile will run.
 
+Sometimes you need to run Task in an environment that does not have an
+interactive terminal, so you are not able to accept a prompt. In these cases you
+are able to tell task to accept these prompts automatically by using the `--yes`
+flag. Before enabling this flag, you should:
+
+1. Be sure that you trust the source and contents of the remote Taskfile.
+2. Consider using a pinned version of the remote Taskfile (e.g. A link
+   containing a commit hash) to prevent Task from automatically accepting a
+   prompt that says a remote Taskfile has changed.
+
 Task currently supports both `http` and `https` URLs. However, the `http`
 requests will not execute by default unless you run the task with the
 `--insecure` flag. This is to protect you from accidentally running a remote
diff --git a/internal/logger/logger.go b/internal/logger/logger.go
index ab031a46..c031346f 100644
--- a/internal/logger/logger.go
+++ b/internal/logger/logger.go
@@ -9,6 +9,14 @@ import (
 
 	"github.com/fatih/color"
 	"golang.org/x/exp/slices"
+
+	"github.com/go-task/task/v3/errors"
+	"github.com/go-task/task/v3/internal/term"
+)
+
+var (
+	ErrPromptCancelled = errors.New("prompt cancelled")
+	ErrNoTerminal      = errors.New("no terminal")
 )
 
 type (
@@ -59,10 +67,13 @@ func envColor(env string, defaultColor color.Attribute) color.Attribute {
 // Logger is just a wrapper that prints stuff to STDOUT or STDERR,
 // with optional color.
 type Logger struct {
-	Stdout  io.Writer
-	Stderr  io.Writer
-	Verbose bool
-	Color   bool
+	Stdin      io.Reader
+	Stdout     io.Writer
+	Stderr     io.Writer
+	Verbose    bool
+	Color      bool
+	AssumeYes  bool
+	AssumeTerm bool // Used for testing
 }
 
 // Outf prints stuff to STDOUT.
@@ -108,16 +119,32 @@ func (l *Logger) VerboseErrf(color Color, s string, args ...any) {
 	}
 }
 
-func (l *Logger) Prompt(color Color, s string, defaultValue string, continueValues ...string) (bool, error) {
+func (l *Logger) Prompt(color Color, prompt string, defaultValue string, continueValues ...string) error {
+	if l.AssumeYes {
+		l.Outf(color, "%s [assuming yes]\n", prompt)
+		return nil
+	}
+
+	if !l.AssumeTerm && !term.IsTerminal() {
+		return ErrNoTerminal
+	}
+
 	if len(continueValues) == 0 {
-		return false, nil
+		return errors.New("no continue values provided")
 	}
-	l.Outf(color, "%s [%s/%s]\n", s, strings.ToLower(continueValues[0]), strings.ToUpper(defaultValue))
-	reader := bufio.NewReader(os.Stdin)
+
+	l.Outf(color, "%s [%s/%s]\n", prompt, strings.ToLower(continueValues[0]), strings.ToUpper(defaultValue))
+
+	reader := bufio.NewReader(l.Stdin)
 	input, err := reader.ReadString('\n')
 	if err != nil {
-		return false, err
+		return err
 	}
+
 	input = strings.TrimSpace(strings.ToLower(input))
-	return slices.Contains(continueValues, input), nil
+	if !slices.Contains(continueValues, input) {
+		return ErrPromptCancelled
+	}
+
+	return nil
 }
diff --git a/setup.go b/setup.go
index c3430e77..afbe88ba 100644
--- a/setup.go
+++ b/setup.go
@@ -157,10 +157,13 @@ func (e *Executor) setupStdFiles() {
 
 func (e *Executor) setupLogger() {
 	e.Logger = &logger.Logger{
-		Stdout:  e.Stdout,
-		Stderr:  e.Stderr,
-		Verbose: e.Verbose,
-		Color:   e.Color,
+		Stdin:      e.Stdin,
+		Stdout:     e.Stdout,
+		Stderr:     e.Stderr,
+		Verbose:    e.Verbose,
+		Color:      e.Color,
+		AssumeYes:  e.AssumeYes,
+		AssumeTerm: e.AssumeTerm,
 	}
 }
 
diff --git a/task.go b/task.go
index f3f67ab6..c8f5d79f 100644
--- a/task.go
+++ b/task.go
@@ -1,13 +1,11 @@
 package task
 
 import (
-	"bufio"
 	"context"
 	"fmt"
 	"io"
 	"os"
 	"runtime"
-	"strings"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -23,7 +21,6 @@ import (
 	"github.com/go-task/task/v3/internal/sort"
 	"github.com/go-task/task/v3/internal/summary"
 	"github.com/go-task/task/v3/internal/templater"
-	"github.com/go-task/task/v3/internal/term"
 	"github.com/go-task/task/v3/taskfile"
 
 	"github.com/sajari/fuzzy"
@@ -37,11 +34,6 @@ const (
 	MaximumTaskCall = 1000
 )
 
-func shouldPromptContinue(input string) bool {
-	input = strings.ToLower(strings.TrimSpace(input))
-	return slices.Contains([]string{"y", "yes"}, input)
-}
-
 // Executor executes a Taskfile
 type Executor struct {
 	Taskfile *taskfile.Taskfile
@@ -58,13 +50,13 @@ type Executor struct {
 	Verbose     bool
 	Silent      bool
 	AssumeYes   bool
+	AssumeTerm  bool // Used for testing
 	Dry         bool
 	Summary     bool
 	Parallel    bool
 	Color       bool
 	Concurrency int
 	Interval    time.Duration
-	AssumesTerm bool
 
 	Stdin  io.Reader
 	Stdout io.Writer
@@ -182,22 +174,13 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
 	release := e.acquireConcurrencyLimit()
 	defer release()
 
-	if t.Prompt != "" && !e.AssumeYes {
-		if !e.AssumesTerm && !term.IsTerminal() {
+	if t.Prompt != "" {
+		if err := e.Logger.Prompt(logger.Yellow, t.Prompt, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
 			return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
-		}
-
-		e.Logger.Outf(logger.Yellow, "task: %q [y/N]: ", t.Prompt)
-
-		reader := bufio.NewReader(e.Stdin)
-		userInput, err := reader.ReadString('\n')
-		if err != nil {
-			return err
-		}
-
-		userInput = strings.ToLower(strings.TrimSpace(userInput))
-		if !shouldPromptContinue(userInput) {
+		} else if errors.Is(err, logger.ErrPromptCancelled) {
 			return &errors.TaskCancelledByUserError{TaskName: call.Task}
+		} else if err != nil {
+			return err
 		}
 	}
 
diff --git a/taskfile/read/taskfile.go b/taskfile/read/taskfile.go
index 89f6f7c4..5abdd1b9 100644
--- a/taskfile/read/taskfile.go
+++ b/taskfile/read/taskfile.go
@@ -86,19 +86,19 @@ func readTaskfile(
 			checksum := checksum(b)
 			cachedChecksum := cache.readChecksum(node)
 
-			// If the checksum doesn't exist, prompt the user to continue
+			var msg string
 			if cachedChecksum == "" {
-				if cont, err := l.Prompt(logger.Yellow, fmt.Sprintf("The task you are attempting to run depends on the remote Taskfile at %q.\n--- Make sure you trust the source of this Taskfile before continuing ---\nContinue?", node.Location()), "n", "y", "yes"); err != nil {
-					return nil, err
-				} else if !cont {
-					return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
-				}
+				// If the checksum doesn't exist, prompt the user to continue
+				msg = fmt.Sprintf("The task you are attempting to run depends on the remote Taskfile at %q.\n--- Make sure you trust the source of this Taskfile before continuing ---\nContinue?", node.Location())
 			} else if checksum != cachedChecksum {
 				// If there is a cached hash, but it doesn't match the expected hash, prompt the user to continue
-				if cont, err := l.Prompt(logger.Yellow, fmt.Sprintf("The Taskfile at %q has changed since you last used it!\n--- Make sure you trust the source of this Taskfile before continuing ---\nContinue?", node.Location()), "n", "y", "yes"); err != nil {
-					return nil, err
-				} else if !cont {
+				msg = fmt.Sprintf("The Taskfile at %q has changed since you last used it!\n--- Make sure you trust the source of this Taskfile before continuing ---\nContinue?", node.Location())
+			}
+			if msg != "" {
+				if err := l.Prompt(logger.Yellow, msg, "n", "y", "yes"); errors.Is(err, logger.ErrPromptCancelled) {
 					return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
+				} else if err != nil {
+					return nil, err
 				}
 			}
 


Test patch:
diff --git a/task_test.go b/task_test.go
index 8370afce..c926adb1 100644
--- a/task_test.go
+++ b/task_test.go
@@ -681,11 +681,11 @@ func TestPromptInSummary(t *testing.T) {
 			inBuff.Write([]byte(test.input))
 
 			e := task.Executor{
-				Dir:         dir,
-				Stdin:       &inBuff,
-				Stdout:      &outBuff,
-				Stderr:      &errBuff,
-				AssumesTerm: true,
+				Dir:        dir,
+				Stdin:      &inBuff,
+				Stdout:     &outBuff,
+				Stderr:     &errBuff,
+				AssumeTerm: true,
 			}
 			require.NoError(t, e.Setup())
 
@@ -709,11 +709,11 @@ func TestPromptWithIndirectTask(t *testing.T) {
 	inBuff.Write([]byte("y\n"))
 
 	e := task.Executor{
-		Dir:         dir,
-		Stdin:       &inBuff,
-		Stdout:      &outBuff,
-		Stderr:      &errBuff,
-		AssumesTerm: true,
+		Dir:        dir,
+		Stdin:      &inBuff,
+		Stdout:     &outBuff,
+		Stderr:     &errBuff,
+		AssumeTerm: true,
 	}
 	require.NoError(t, e.Setup())