1package termio 2 3import ( 4 "context" 5 "fmt" 6 "io" 7 "os" 8 "strconv" 9 "strings" 10 11 "github.com/gopasspw/gopass/internal/out" 12 "github.com/gopasspw/gopass/pkg/ctxutil" 13) 14 15var ( 16 // Stderr is exported for tests 17 Stderr io.Writer = os.Stderr 18 // Stdin is exported for tests 19 Stdin io.Reader = os.Stdin 20 // ErrAborted is returned if the user aborts an action 21 ErrAborted = fmt.Errorf("user aborted") 22) 23 24const ( 25 maxTries = 42 26) 27 28// AskForString asks for a string once, using the default if the 29// answer is empty. Errors are only returned on I/O errors 30func AskForString(ctx context.Context, text, def string) (string, error) { 31 if ctxutil.IsAlwaysYes(ctx) || !ctxutil.IsInteractive(ctx) { 32 return def, nil 33 } 34 35 // check for context cancelation 36 select { 37 case <-ctx.Done(): 38 return def, ErrAborted 39 default: 40 } 41 42 fmt.Fprintf(Stderr, "%s [%s]: ", text, def) 43 input, err := NewReader(ctx, Stdin).ReadLine() 44 if err != nil { 45 return "", fmt.Errorf("failed to read user input: %w", err) 46 } 47 input = strings.TrimSpace(input) 48 if input == "" { 49 input = def 50 } 51 return input, nil 52} 53 54// AskForBool ask for a bool (yes or no) exactly once. 55// The empty answer uses the specified default, any other answer 56// is an error. 57func AskForBool(ctx context.Context, text string, def bool) (bool, error) { 58 if ctxutil.IsAlwaysYes(ctx) { 59 return def, nil 60 } 61 62 choices := "y/N/q" 63 if def { 64 choices = "Y/n/q" 65 } 66 67 str, err := AskForString(ctx, text, choices) 68 if err != nil { 69 return false, fmt.Errorf("failed to read user input: %w", err) 70 } 71 switch str { 72 case "Y/n/q": 73 return true, nil 74 case "y/N/q": 75 return false, nil 76 } 77 78 str = strings.ToLower(string(str[0])) 79 switch str { 80 case "y": 81 return true, nil 82 case "n": 83 return false, nil 84 case "q": 85 return false, ErrAborted 86 default: 87 return false, fmt.Errorf("unknown answer: %s", str) 88 } 89} 90 91// AskForInt asks for an valid interger once. If the input 92// can not be converted to an int it returns an error 93func AskForInt(ctx context.Context, text string, def int) (int, error) { 94 if ctxutil.IsAlwaysYes(ctx) { 95 return def, nil 96 } 97 98 str, err := AskForString(ctx, text+" (q to abort)", strconv.Itoa(def)) 99 if err != nil { 100 return 0, err 101 } 102 if str == "q" { 103 return 0, ErrAborted 104 } 105 intVal, err := strconv.Atoi(str) 106 if err != nil { 107 return 0, fmt.Errorf("failed to convert to number: %w", err) 108 } 109 return intVal, nil 110} 111 112// AskForConfirmation asks a yes/no question until the user 113// replies yes or no 114func AskForConfirmation(ctx context.Context, text string) bool { 115 if ctxutil.IsAlwaysYes(ctx) { 116 return true 117 } 118 119 for i := 0; i < maxTries; i++ { 120 choice, err := AskForBool(ctx, text, false) 121 if err == nil { 122 return choice 123 } 124 if err == ErrAborted { 125 return false 126 } 127 } 128 return false 129} 130 131// AskForKeyImport asks for permissions to import the named key 132func AskForKeyImport(ctx context.Context, key string, names []string) bool { 133 if ctxutil.IsAlwaysYes(ctx) { 134 return true 135 } 136 if !ctxutil.IsInteractive(ctx) { 137 return false 138 } 139 140 ok, err := AskForBool(ctx, fmt.Sprintf("Do you want to import the public key %q (Names: %+v) into your keyring?", key, names), false) 141 if err != nil { 142 return false 143 } 144 145 return ok 146} 147 148// AskForPassword prompts for a password, optionally prompting twice until both match 149func AskForPassword(ctx context.Context, name string, repeat bool) (string, error) { 150 if ctxutil.IsAlwaysYes(ctx) { 151 return "", nil 152 } 153 154 askFn := GetPassPromptFunc(ctx) 155 for i := 0; i < maxTries; i++ { 156 // check for context cancellation 157 select { 158 case <-ctx.Done(): 159 return "", ErrAborted 160 default: 161 } 162 163 pass, err := askFn(ctx, fmt.Sprintf("Enter %s", name)) 164 if !repeat { 165 return pass, err 166 } 167 if err != nil { 168 return "", err 169 } 170 171 passAgain, err := askFn(ctx, fmt.Sprintf("Retype %s", name)) 172 if err != nil { 173 return "", err 174 } 175 176 if pass == passAgain { 177 return pass, nil 178 } 179 180 out.Errorf(ctx, "Error: the entered password do not match") 181 } 182 return "", fmt.Errorf("no valid user input") 183} 184