mirror of https://github.com/knative/func.git
221 lines
4.1 KiB
Go
221 lines
4.1 KiB
Go
package prompt
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Default delimiter between prompt and user input. Includes a space such that
|
|
// overrides have full control of spacing.
|
|
const DefaultDelimiter = ": "
|
|
|
|
// DefaultRetryLimit imposed for a few reasons, not least of which infinite
|
|
// loops in tests.
|
|
const DefaultRetryLimit = 10
|
|
|
|
type prompt struct {
|
|
in io.Reader
|
|
out io.Writer
|
|
label string
|
|
delim string
|
|
required bool
|
|
retryLimit int
|
|
}
|
|
|
|
type stringPrompt struct {
|
|
prompt
|
|
dflt string
|
|
}
|
|
|
|
type boolPrompt struct {
|
|
prompt
|
|
dflt bool
|
|
}
|
|
|
|
type Option func(*prompt)
|
|
|
|
func WithInput(in io.Reader) Option {
|
|
return func(p *prompt) {
|
|
p.in = in
|
|
}
|
|
}
|
|
|
|
func WithOutput(out io.Writer) Option {
|
|
return func(p *prompt) {
|
|
p.out = out
|
|
}
|
|
}
|
|
|
|
func WithDelimiter(d string) Option {
|
|
return func(p *prompt) {
|
|
p.delim = d
|
|
}
|
|
}
|
|
|
|
func WithRequired(r bool) Option {
|
|
return func(p *prompt) {
|
|
p.required = r
|
|
}
|
|
}
|
|
|
|
func WithRetryLimit(l int) Option {
|
|
return func(p *prompt) {
|
|
p.retryLimit = l
|
|
}
|
|
}
|
|
|
|
func ForString(label string, dflt string, options ...Option) string {
|
|
p := &stringPrompt{
|
|
prompt: prompt{
|
|
in: os.Stdin,
|
|
out: os.Stdout,
|
|
label: label,
|
|
delim: DefaultDelimiter,
|
|
retryLimit: DefaultRetryLimit,
|
|
},
|
|
dflt: dflt,
|
|
}
|
|
for _, o := range options {
|
|
o(&p.prompt)
|
|
}
|
|
|
|
writeStringLabel(p) // Write the label
|
|
input, err := readString(p) // gather the input
|
|
var attempt int
|
|
for err != nil && attempt < p.retryLimit { // while there are errors
|
|
attempt++
|
|
writeError(err, &p.prompt) // write the error on its own line
|
|
writeStringLabel(p) // re-write the label
|
|
input, err = readString(p) // re-read the input
|
|
}
|
|
return input
|
|
}
|
|
|
|
func writeStringLabel(p *stringPrompt) {
|
|
_, err := p.out.Write([]byte(p.label))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if p.dflt != "" {
|
|
if p.label != "" {
|
|
_, err = p.out.Write([]byte(" "))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
fmt.Fprintf(p.out, "(%v)", p.dflt)
|
|
}
|
|
_, err = p.out.Write([]byte(p.delim))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func readString(p *stringPrompt) (s string, err error) {
|
|
if s, err = bufio.NewReader(p.in).ReadString('\n'); err != nil {
|
|
return
|
|
}
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
s = p.dflt
|
|
}
|
|
if s == "" && p.required {
|
|
err = errors.New("please enter a value")
|
|
}
|
|
return
|
|
}
|
|
|
|
func ForBool(label string, dflt bool, options ...Option) bool {
|
|
p := &boolPrompt{
|
|
prompt: prompt{
|
|
in: os.Stdin,
|
|
out: os.Stdout,
|
|
label: label,
|
|
delim: DefaultDelimiter,
|
|
retryLimit: DefaultRetryLimit,
|
|
},
|
|
dflt: dflt,
|
|
}
|
|
for _, o := range options {
|
|
o(&p.prompt)
|
|
}
|
|
|
|
writeBoolLabel(p) // write the prompt label
|
|
input, err := readBool(p) // gather the input
|
|
var attempt int
|
|
for err != nil && attempt < p.retryLimit {
|
|
attempt++
|
|
writeError(err, &p.prompt) // write the error on its own line
|
|
writeBoolLabel(p) // re-write the label
|
|
input, err = readBool(p) // re-read the input
|
|
}
|
|
|
|
return input
|
|
}
|
|
|
|
func writeBoolLabel(p *boolPrompt) {
|
|
_, err := p.out.Write([]byte(p.label))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if p.label != "" {
|
|
_, err = p.out.Write([]byte(" "))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
if p.dflt {
|
|
fmt.Fprint(p.out, "(Y/n)")
|
|
} else {
|
|
fmt.Fprint(p.out, "(y/N)")
|
|
}
|
|
_, err = p.out.Write([]byte(p.delim))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func readBool(p *boolPrompt) (bool, error) {
|
|
reader := bufio.NewReader(p.in)
|
|
var s string
|
|
s, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return p.dflt, err
|
|
}
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return p.dflt, nil
|
|
}
|
|
if isTruthy(s) { // variants of 'true'
|
|
return true, nil
|
|
} else if isFalsy(s) {
|
|
return false, nil
|
|
}
|
|
return strconv.ParseBool(s)
|
|
}
|
|
|
|
var truthy = regexp.MustCompile("(?i)y(?:es)?|1")
|
|
var falsy = regexp.MustCompile("(?i)n(?:o)?|0")
|
|
|
|
func isTruthy(confirm string) bool {
|
|
return truthy.MatchString(confirm)
|
|
}
|
|
|
|
func isFalsy(confirm string) bool {
|
|
return falsy.MatchString(confirm)
|
|
}
|
|
|
|
func writeError(err error, p *prompt) {
|
|
_, _err := p.out.Write([]byte("\n"))
|
|
if _err != nil {
|
|
panic(_err)
|
|
}
|
|
fmt.Fprintln(p.out, err)
|
|
}
|