feat: unify prompts (#1344)

This commit is contained in:
Pete Davison
2023-10-07 16:55:43 -05:00
committed by GitHub
parent 222cd8c8f8
commit dc77286282
7 changed files with 83 additions and 57 deletions

View File

@@ -2,6 +2,9 @@
## Unreleased ## 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 - Add ability to set `watch: true` in a task to automatically run it in watch
mode (#231, #1361 by @andreynering). mode (#231, #1361 by @andreynering).
- Fixed a bug on the watch mode where paths that contained `.git` (like - Fixed a bug on the watch mode where paths that contained `.git` (like

View File

@@ -54,6 +54,16 @@ Taskfiles:
code `104` (not trusted) and nothing will run. If you accept the prompt, the code `104` (not trusted) and nothing will run. If you accept the prompt, the
checksum will be updated and the remote Taskfile will run. 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` Task currently supports both `http` and `https` URLs. However, the `http`
requests will not execute by default unless you run the task with the 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 `--insecure` flag. This is to protect you from accidentally running a remote

View File

@@ -9,6 +9,14 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
"golang.org/x/exp/slices" "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 ( 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, // Logger is just a wrapper that prints stuff to STDOUT or STDERR,
// with optional color. // with optional color.
type Logger struct { type Logger struct {
Stdout io.Writer Stdin io.Reader
Stderr io.Writer Stdout io.Writer
Verbose bool Stderr io.Writer
Color bool Verbose bool
Color bool
AssumeYes bool
AssumeTerm bool // Used for testing
} }
// Outf prints stuff to STDOUT. // 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 len(continueValues) == 0 { if l.AssumeYes {
return false, nil l.Outf(color, "%s [assuming yes]\n", prompt)
return nil
} }
l.Outf(color, "%s [%s/%s]\n", s, strings.ToLower(continueValues[0]), strings.ToUpper(defaultValue))
reader := bufio.NewReader(os.Stdin) if !l.AssumeTerm && !term.IsTerminal() {
return ErrNoTerminal
}
if len(continueValues) == 0 {
return errors.New("no continue values provided")
}
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') input, err := reader.ReadString('\n')
if err != nil { if err != nil {
return false, err return err
} }
input = strings.TrimSpace(strings.ToLower(input)) input = strings.TrimSpace(strings.ToLower(input))
return slices.Contains(continueValues, input), nil if !slices.Contains(continueValues, input) {
return ErrPromptCancelled
}
return nil
} }

View File

@@ -157,10 +157,13 @@ func (e *Executor) setupStdFiles() {
func (e *Executor) setupLogger() { func (e *Executor) setupLogger() {
e.Logger = &logger.Logger{ e.Logger = &logger.Logger{
Stdout: e.Stdout, Stdin: e.Stdin,
Stderr: e.Stderr, Stdout: e.Stdout,
Verbose: e.Verbose, Stderr: e.Stderr,
Color: e.Color, Verbose: e.Verbose,
Color: e.Color,
AssumeYes: e.AssumeYes,
AssumeTerm: e.AssumeTerm,
} }
} }

29
task.go
View File

@@ -1,13 +1,11 @@
package task package task
import ( import (
"bufio"
"context" "context"
"fmt" "fmt"
"io" "io"
"os" "os"
"runtime" "runtime"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -23,7 +21,6 @@ import (
"github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/internal/sort"
"github.com/go-task/task/v3/internal/summary" "github.com/go-task/task/v3/internal/summary"
"github.com/go-task/task/v3/internal/templater" "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/go-task/task/v3/taskfile"
"github.com/sajari/fuzzy" "github.com/sajari/fuzzy"
@@ -37,11 +34,6 @@ const (
MaximumTaskCall = 1000 MaximumTaskCall = 1000
) )
func shouldPromptContinue(input string) bool {
input = strings.ToLower(strings.TrimSpace(input))
return slices.Contains([]string{"y", "yes"}, input)
}
// Executor executes a Taskfile // Executor executes a Taskfile
type Executor struct { type Executor struct {
Taskfile *taskfile.Taskfile Taskfile *taskfile.Taskfile
@@ -58,13 +50,13 @@ type Executor struct {
Verbose bool Verbose bool
Silent bool Silent bool
AssumeYes bool AssumeYes bool
AssumeTerm bool // Used for testing
Dry bool Dry bool
Summary bool Summary bool
Parallel bool Parallel bool
Color bool Color bool
Concurrency int Concurrency int
Interval time.Duration Interval time.Duration
AssumesTerm bool
Stdin io.Reader Stdin io.Reader
Stdout io.Writer Stdout io.Writer
@@ -182,22 +174,13 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
release := e.acquireConcurrencyLimit() release := e.acquireConcurrencyLimit()
defer release() defer release()
if t.Prompt != "" && !e.AssumeYes { if t.Prompt != "" {
if !e.AssumesTerm && !term.IsTerminal() { if err := e.Logger.Prompt(logger.Yellow, t.Prompt, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
return &errors.TaskCancelledNoTerminalError{TaskName: call.Task} return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
} } else if errors.Is(err, logger.ErrPromptCancelled) {
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) {
return &errors.TaskCancelledByUserError{TaskName: call.Task} return &errors.TaskCancelledByUserError{TaskName: call.Task}
} else if err != nil {
return err
} }
} }

View File

@@ -681,11 +681,11 @@ func TestPromptInSummary(t *testing.T) {
inBuff.Write([]byte(test.input)) inBuff.Write([]byte(test.input))
e := task.Executor{ e := task.Executor{
Dir: dir, Dir: dir,
Stdin: &inBuff, Stdin: &inBuff,
Stdout: &outBuff, Stdout: &outBuff,
Stderr: &errBuff, Stderr: &errBuff,
AssumesTerm: true, AssumeTerm: true,
} }
require.NoError(t, e.Setup()) require.NoError(t, e.Setup())
@@ -709,11 +709,11 @@ func TestPromptWithIndirectTask(t *testing.T) {
inBuff.Write([]byte("y\n")) inBuff.Write([]byte("y\n"))
e := task.Executor{ e := task.Executor{
Dir: dir, Dir: dir,
Stdin: &inBuff, Stdin: &inBuff,
Stdout: &outBuff, Stdout: &outBuff,
Stderr: &errBuff, Stderr: &errBuff,
AssumesTerm: true, AssumeTerm: true,
} }
require.NoError(t, e.Setup()) require.NoError(t, e.Setup())

View File

@@ -86,19 +86,19 @@ func readTaskfile(
checksum := checksum(b) checksum := checksum(b)
cachedChecksum := cache.readChecksum(node) cachedChecksum := cache.readChecksum(node)
// If the checksum doesn't exist, prompt the user to continue var msg string
if cachedChecksum == "" { 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 { // If the checksum doesn't exist, prompt the user to continue
return nil, err 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 !cont {
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
}
} else if checksum != cachedChecksum { } else if checksum != cachedChecksum {
// If there is a cached hash, but it doesn't match the expected hash, prompt the user to continue // 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 { 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())
return nil, err }
} else if !cont { if msg != "" {
if err := l.Prompt(logger.Yellow, msg, "n", "y", "yes"); errors.Is(err, logger.ErrPromptCancelled) {
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()} return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
} else if err != nil {
return nil, err
} }
} }