mirror of https://github.com/knative/func.git
src: refactor prompt to use `AlecAivazis/survey` (#397)
* src: refactor prompt to use `AlecAivazis/survey` Signed-off-by: Zbynek Roubalik <zroubali@redhat.com>
This commit is contained in:
parent
83a9ca684f
commit
7a24a103ef
58
cmd/build.go
58
cmd/build.go
|
@ -3,13 +3,14 @@ package cmd
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/ory/viper"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
bosonFunc "github.com/boson-project/func"
|
||||
"github.com/boson-project/func/buildpacks"
|
||||
"github.com/boson-project/func/progress"
|
||||
"github.com/boson-project/func/prompt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -58,7 +59,13 @@ kn func build --builder cnbs/sample-builder:bionic
|
|||
}
|
||||
|
||||
func runBuild(cmd *cobra.Command, _ []string) (err error) {
|
||||
config := newBuildConfig().Prompt()
|
||||
config, err := newBuildConfig().Prompt()
|
||||
if err != nil {
|
||||
if err == terminal.InterruptErr {
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
function, err := functionWithOverrides(config.Path, functionOverrides{Builder: config.Builder, Image: config.Image})
|
||||
if err != nil {
|
||||
|
@ -75,10 +82,16 @@ func runBuild(cmd *cobra.Command, _ []string) (err error) {
|
|||
// AND a --registry was not provided, then we need to
|
||||
// prompt for a registry from which we can derive an image name.
|
||||
if config.Registry == "" {
|
||||
fmt.Print("A registry for function images is required (e.g. 'quay.io/boson').\n\n")
|
||||
config.Registry = prompt.ForString("Registry for function images", "")
|
||||
if config.Registry == "" {
|
||||
return fmt.Errorf("unable to determine function image name")
|
||||
fmt.Println("A registry for Function images is required. For example, 'docker.io/tigerteam'.")
|
||||
|
||||
err = survey.AskOne(
|
||||
&survey.Input{Message: "Registry for Function images:"},
|
||||
&config.Registry, survey.WithValidator(survey.Required))
|
||||
if err != nil {
|
||||
if err == terminal.InterruptErr {
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,16 +167,33 @@ func newBuildConfig() buildConfig {
|
|||
// Prompt the user with value of config members, allowing for interaractive changes.
|
||||
// Skipped if not in an interactive terminal (non-TTY), or if --confirm false (agree to
|
||||
// all prompts) was set (default).
|
||||
func (c buildConfig) Prompt() buildConfig {
|
||||
func (c buildConfig) Prompt() (buildConfig, error) {
|
||||
imageName := deriveImage(c.Image, c.Registry, c.Path)
|
||||
if !interactiveTerminal() || !c.Confirm {
|
||||
return c
|
||||
return c, nil
|
||||
}
|
||||
return buildConfig{
|
||||
Path: prompt.ForString("Path to project directory", c.Path),
|
||||
Image: prompt.ForString("Full image name (e.g. quay.io/boson/node-sample)", imageName, prompt.WithRequired(true)),
|
||||
Verbose: c.Verbose,
|
||||
// Registry not prompted for as it would be confusing when combined with explicit image. Instead it is
|
||||
// inferred by the derived default for Image, which uses Registry for derivation.
|
||||
|
||||
bc := buildConfig{Verbose: c.Verbose}
|
||||
|
||||
var qs = []*survey.Question{
|
||||
{
|
||||
Name: "path",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Project path:",
|
||||
Default: c.Path,
|
||||
},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
{
|
||||
Name: "image",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Full image name (e.g. quay.io/boson/node-sample):",
|
||||
Default: imageName,
|
||||
},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
}
|
||||
err := survey.Ask(qs, &bc)
|
||||
|
||||
return bc, err
|
||||
}
|
||||
|
|
|
@ -4,12 +4,13 @@ import (
|
|||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/ory/viper"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
bosonFunc "github.com/boson-project/func"
|
||||
"github.com/boson-project/func/buildpacks"
|
||||
"github.com/boson-project/func/prompt"
|
||||
"github.com/boson-project/func/utils"
|
||||
)
|
||||
|
||||
|
@ -52,14 +53,21 @@ kn func create --template events myfunc
|
|||
// TODO: autocomplate or interactive prompt for runtime and template.
|
||||
}
|
||||
|
||||
func runCreate(cmd *cobra.Command, args []string) error {
|
||||
func runCreate(cmd *cobra.Command, args []string) (err error) {
|
||||
config := newCreateConfig(args)
|
||||
|
||||
if err := utils.ValidateFunctionName(config.Name); err != nil {
|
||||
return err
|
||||
err = utils.ValidateFunctionName(config.Name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
config = config.Prompt()
|
||||
config, err = config.Prompt()
|
||||
if err != nil {
|
||||
if err == terminal.InterruptErr {
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
function := bosonFunc.Function{
|
||||
Name: config.Name,
|
||||
|
@ -131,30 +139,62 @@ func newCreateConfig(args []string) createConfig {
|
|||
// Prompt the user with value of config members, allowing for interaractive changes.
|
||||
// Skipped if not in an interactive terminal (non-TTY), or if --confirm false (agree to
|
||||
// all prompts) was set (default).
|
||||
func (c createConfig) Prompt() createConfig {
|
||||
func (c createConfig) Prompt() (createConfig, error) {
|
||||
if !interactiveTerminal() || !c.Confirm {
|
||||
// Just print the basics if not confirming
|
||||
fmt.Printf("Project path: %v\n", c.Path)
|
||||
fmt.Printf("Function name: %v\n", c.Name)
|
||||
fmt.Printf("Runtime: %v\n", c.Runtime)
|
||||
fmt.Printf("Template: %v\n", c.Template)
|
||||
return c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
var derivedName, derivedPath string
|
||||
for {
|
||||
derivedName, derivedPath = deriveNameAndAbsolutePathFromPath(prompt.ForString("Project path", c.Path, prompt.WithRequired(true)))
|
||||
err := utils.ValidateFunctionName(derivedName)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
fmt.Println("Error:", err)
|
||||
var qs = []*survey.Question{
|
||||
{
|
||||
Name: "path",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Project path:",
|
||||
Default: c.Path,
|
||||
},
|
||||
Validate: func(val interface{}) error {
|
||||
derivedName, _ := deriveNameAndAbsolutePathFromPath(val.(string))
|
||||
return utils.ValidateFunctionName(derivedName)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "runtime",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Runtime:",
|
||||
Default: c.Runtime,
|
||||
// TODO add runtime suggestions: https://github.com/AlecAivazis/survey#suggestion-options
|
||||
},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
{
|
||||
Name: "template",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Template:",
|
||||
Default: c.Template,
|
||||
// TODO add template suggestions: https://github.com/AlecAivazis/survey#suggestion-options
|
||||
},
|
||||
},
|
||||
}
|
||||
answers := struct {
|
||||
Template string
|
||||
Runtime string
|
||||
Path string
|
||||
}{}
|
||||
err := survey.Ask(qs, &answers)
|
||||
if err != nil {
|
||||
return createConfig{}, err
|
||||
}
|
||||
|
||||
derivedName, derivedPath := deriveNameAndAbsolutePathFromPath(answers.Path)
|
||||
|
||||
return createConfig{
|
||||
Name: derivedName,
|
||||
Path: derivedPath,
|
||||
Runtime: prompt.ForString("Runtime", c.Runtime),
|
||||
Template: prompt.ForString("Template", c.Template),
|
||||
}
|
||||
Runtime: answers.Runtime,
|
||||
Template: answers.Template,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -3,12 +3,13 @@ package cmd
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/ory/viper"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
fn "github.com/boson-project/func"
|
||||
"github.com/boson-project/func/knative"
|
||||
"github.com/boson-project/func/prompt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -38,7 +39,13 @@ kn func delete -n apps myfunc
|
|||
ValidArgsFunction: CompleteFunctionList,
|
||||
PreRunE: bindEnv("path", "confirm", "namespace"),
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
config := newDeleteConfig(args).Prompt()
|
||||
config, err := newDeleteConfig(args).Prompt()
|
||||
if err != nil {
|
||||
if err == terminal.InterruptErr {
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var function fn.Function
|
||||
|
||||
|
@ -122,12 +129,16 @@ func newDeleteConfig(args []string) deleteConfig {
|
|||
// Prompt the user with value of config members, allowing for interaractive changes.
|
||||
// Skipped if not in an interactive terminal (non-TTY), or if --yes (agree to
|
||||
// all prompts) was explicitly set.
|
||||
func (c deleteConfig) Prompt() deleteConfig {
|
||||
func (c deleteConfig) Prompt() (deleteConfig, error) {
|
||||
if !interactiveTerminal() || !viper.GetBool("confirm") {
|
||||
return c
|
||||
}
|
||||
return deleteConfig{
|
||||
// TODO: Path should be prompted for and set prior to name attempting path derivation. Test/fix this if necessary.
|
||||
Name: prompt.ForString("Function to remove", deriveName(c.Name, c.Path), prompt.WithRequired(true)),
|
||||
return c, nil
|
||||
}
|
||||
|
||||
dc := deleteConfig{}
|
||||
|
||||
return dc, survey.AskOne(
|
||||
&survey.Input{
|
||||
Message: "Function to remove:",
|
||||
Default: deriveName(c.Name, c.Path)},
|
||||
&dc.Name, survey.WithValidator(survey.Required))
|
||||
}
|
||||
|
|
163
cmd/deploy.go
163
cmd/deploy.go
|
@ -1,19 +1,16 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/containers/image/v5/pkg/docker/config"
|
||||
containersTypes "github.com/containers/image/v5/types"
|
||||
"github.com/ory/viper"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
"knative.dev/client/pkg/util"
|
||||
|
||||
bosonFunc "github.com/boson-project/func"
|
||||
|
@ -21,7 +18,6 @@ import (
|
|||
"github.com/boson-project/func/docker"
|
||||
"github.com/boson-project/func/knative"
|
||||
"github.com/boson-project/func/progress"
|
||||
"github.com/boson-project/func/prompt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -72,7 +68,13 @@ func runDeploy(cmd *cobra.Command, _ []string) (err error) {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config = config.Prompt()
|
||||
config, err = config.Prompt()
|
||||
if err != nil {
|
||||
if err == terminal.InterruptErr {
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
function, err := functionWithOverrides(config.Path, functionOverrides{Namespace: config.Namespace, Image: config.Image})
|
||||
if err != nil {
|
||||
|
@ -94,10 +96,16 @@ func runDeploy(cmd *cobra.Command, _ []string) (err error) {
|
|||
// AND a --registry was not provided, then we need to
|
||||
// prompt for a registry from which we can derive an image name.
|
||||
if config.Registry == "" {
|
||||
fmt.Print("A registry for Function images is required. For example, 'docker.io/tigerteam'.\n\n")
|
||||
config.Registry = prompt.ForString("Registry for Function images", "")
|
||||
if config.Registry == "" {
|
||||
return fmt.Errorf("unable to determine function image name")
|
||||
fmt.Println("A registry for Function images is required. For example, 'docker.io/tigerteam'.")
|
||||
|
||||
err = survey.AskOne(
|
||||
&survey.Input{Message: "Registry for Function images:"},
|
||||
&config.Registry, survey.WithValidator(survey.Required))
|
||||
if err != nil {
|
||||
if err == terminal.InterruptErr {
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,6 +125,9 @@ func runDeploy(cmd *cobra.Command, _ []string) (err error) {
|
|||
|
||||
pusher, err := docker.NewPusher(docker.WithCredentialsProvider(credentialsProvider))
|
||||
if err != nil {
|
||||
if err == terminal.InterruptErr {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
pusher.Verbose = config.Verbose
|
||||
|
@ -176,72 +187,26 @@ func credentialsProvider(ctx context.Context, registry string) (docker.Credentia
|
|||
return result, nil
|
||||
}
|
||||
|
||||
fmt.Print("Username: ")
|
||||
username, err := getUserName(ctx)
|
||||
if err != nil {
|
||||
return result, err
|
||||
fmt.Println("Please provide credentials for image registry.")
|
||||
var qs = []*survey.Question{
|
||||
{
|
||||
Name: "username",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Username:",
|
||||
},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
{
|
||||
Name: "namespace",
|
||||
Prompt: &survey.Password{
|
||||
Message: "Password:",
|
||||
},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
}
|
||||
err = survey.Ask(qs, &result)
|
||||
|
||||
fmt.Print("Password: ")
|
||||
bytePassword, err := getPassword(ctx)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
password := string(bytePassword)
|
||||
|
||||
result.Username, result.Password = username, password
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getPassword(ctx context.Context) ([]byte, error) {
|
||||
ch := make(chan struct {
|
||||
p []byte
|
||||
e error
|
||||
})
|
||||
|
||||
go func() {
|
||||
pass, err := term.ReadPassword(int(syscall.Stdin)) // nolint: unconvert
|
||||
ch <- struct {
|
||||
p []byte
|
||||
e error
|
||||
}{p: pass, e: err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case res := <-ch:
|
||||
return res.p, res.e
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func getUserName(ctx context.Context) (string, error) {
|
||||
ch := make(chan struct {
|
||||
u string
|
||||
e error
|
||||
})
|
||||
go func() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
username, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
ch <- struct {
|
||||
u string
|
||||
e error
|
||||
}{u: "", e: err}
|
||||
}
|
||||
ch <- struct {
|
||||
u string
|
||||
e error
|
||||
}{u: strings.TrimRight(username, "\r\n"), e: nil}
|
||||
}()
|
||||
|
||||
select {
|
||||
case res := <-ch:
|
||||
return res.u, res.e
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
type deployConfig struct {
|
||||
|
@ -299,20 +264,56 @@ func newDeployConfig(cmd *cobra.Command) (deployConfig, error) {
|
|||
// Prompt the user with value of config members, allowing for interaractive changes.
|
||||
// Skipped if not in an interactive terminal (non-TTY), or if --yes (agree to
|
||||
// all prompts) was explicitly set.
|
||||
func (c deployConfig) Prompt() deployConfig {
|
||||
func (c deployConfig) Prompt() (deployConfig, error) {
|
||||
if !interactiveTerminal() || !c.Confirm {
|
||||
return c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
var qs = []*survey.Question{
|
||||
{
|
||||
Name: "registry",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Registry for Function images:",
|
||||
Default: c.buildConfig.Registry,
|
||||
},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
{
|
||||
Name: "namespace",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Namespace:",
|
||||
Default: c.Namespace,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "path",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Project path:",
|
||||
Default: c.Path,
|
||||
},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
}
|
||||
answers := struct {
|
||||
Registry string
|
||||
Namespace string
|
||||
Path string
|
||||
}{}
|
||||
err := survey.Ask(qs, &answers)
|
||||
if err != nil {
|
||||
return deployConfig{}, err
|
||||
}
|
||||
|
||||
dc := deployConfig{
|
||||
buildConfig: buildConfig{
|
||||
Registry: prompt.ForString("Registry for Function images", c.buildConfig.Registry),
|
||||
Registry: answers.Registry,
|
||||
},
|
||||
Namespace: prompt.ForString("Namespace", c.Namespace),
|
||||
Path: prompt.ForString("Project path", c.Path),
|
||||
Namespace: answers.Namespace,
|
||||
Path: answers.Path,
|
||||
Verbose: c.Verbose,
|
||||
}
|
||||
|
||||
dc.Image = deriveImage(dc.Image, dc.Registry, dc.Path)
|
||||
|
||||
return dc
|
||||
return dc, nil
|
||||
}
|
||||
|
|
1
go.mod
1
go.mod
|
@ -17,7 +17,6 @@ require (
|
|||
github.com/ory/viper v1.7.4
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/spf13/cobra v1.1.3
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.19.7
|
||||
k8s.io/apimachinery v0.19.7
|
||||
|
|
3
go.sum
3
go.sum
|
@ -98,6 +98,7 @@ github.com/Microsoft/hcsshim v0.8.10/go.mod h1:g5uw8EV2mAlzqe94tfNBNdr89fnbD/n3H
|
|||
github.com/Microsoft/hcsshim v0.8.14 h1:lbPVK25c1cu5xTLITwpUcxoA9vKrKErASPYygvouJns=
|
||||
github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg=
|
||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
|
||||
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
|
@ -547,6 +548,7 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
|
|||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/heroku/color v0.0.6 h1:UTFFMrmMLFcL3OweqP1lAdp8i1y/9oHqkeHjQ/b/Ny0=
|
||||
github.com/heroku/color v0.0.6/go.mod h1:ZBvOcx7cTF2QKOv4LbmoBtNl5uB17qWxGuzZrsi1wLU=
|
||||
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
|
||||
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||
|
@ -610,6 +612,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
|||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
|
|
220
prompt/prompt.go
220
prompt/prompt.go
|
@ -1,220 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -1,247 +0,0 @@
|
|||
// +build !integration
|
||||
|
||||
package prompt_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/boson-project/func/prompt"
|
||||
)
|
||||
|
||||
// TestForStringLabel ensures that a string prompt with a given label is printed to stdout.
|
||||
func TestForStringLabel(t *testing.T) {
|
||||
|
||||
var out bytes.Buffer
|
||||
var in bytes.Buffer
|
||||
in.Write([]byte("\n"))
|
||||
|
||||
// Empty label
|
||||
_ = prompt.ForString("", "",
|
||||
prompt.WithInput(&in), prompt.WithOutput(&out))
|
||||
if out.String() != ": " {
|
||||
t.Fatalf("expected output to be ': ', got '%v'\n", out.String())
|
||||
}
|
||||
|
||||
out.Reset()
|
||||
in.Reset()
|
||||
|
||||
in.Write([]byte("\n"))
|
||||
|
||||
// Populated lable
|
||||
_ = prompt.ForString("Name", "",
|
||||
prompt.WithInput(&in), prompt.WithOutput(&out))
|
||||
if out.String() != "Name: " {
|
||||
t.Fatalf("expected 'Name', got '%v'\n", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestForStringLabelDefault ensures that a default, only if provided, is appended
|
||||
// to the prompt label.
|
||||
func TestForStringLabelDefault(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
var in bytes.Buffer
|
||||
in.Write([]byte("\n")) // [ENTER]
|
||||
|
||||
// No lablel but a default
|
||||
_ = prompt.ForString("", "Alice",
|
||||
prompt.WithInput(&in), prompt.WithOutput(&out))
|
||||
if out.String() != "(Alice): " {
|
||||
t.Fatalf("expected '(Alice): ', got '%v'\n", out.String())
|
||||
}
|
||||
|
||||
out.Reset()
|
||||
in.Reset()
|
||||
in.Write([]byte("\n")) // [ENTER]
|
||||
|
||||
// Label with default
|
||||
_ = prompt.ForString("Name", "Alice",
|
||||
prompt.WithInput(&in), prompt.WithOutput(&out))
|
||||
if out.String() != "Name (Alice): " {
|
||||
t.Fatalf("expected 'Name (Alice): ', got '%v'\n", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestForStringLabelDelimiter ensures that a default delimiter override is respected.
|
||||
func TestWithDelimiter(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
var in bytes.Buffer
|
||||
in.Write([]byte("\n")) // [ENTER]
|
||||
|
||||
_ = prompt.ForString("", "",
|
||||
prompt.WithInput(&in),
|
||||
prompt.WithOutput(&out),
|
||||
prompt.WithDelimiter("Δ"))
|
||||
if out.String() != "Δ" {
|
||||
t.Fatalf("expected output to be 'Δ', got '%v'\n", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestForStringDefault ensures that the default is returned when enter is
|
||||
// pressed on a string input.
|
||||
func TestForStringDefault(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
var in bytes.Buffer
|
||||
in.Write([]byte("\n")) // [ENTER]
|
||||
|
||||
// Empty default should return an empty value.
|
||||
s := prompt.ForString("", "",
|
||||
prompt.WithInput(&in),
|
||||
prompt.WithOutput(&out))
|
||||
|
||||
if s != "" {
|
||||
t.Fatalf("expected '', got '%v'\n", s)
|
||||
}
|
||||
|
||||
in.Reset()
|
||||
out.Reset()
|
||||
in.Write([]byte("\n")) // [ENTER]
|
||||
|
||||
// Extant default should be returned
|
||||
s = prompt.ForString("", "default",
|
||||
prompt.WithInput(&in),
|
||||
prompt.WithOutput(&out))
|
||||
|
||||
if s != "default" {
|
||||
t.Fatalf("expected 'default', got '%v'\n", s)
|
||||
}
|
||||
}
|
||||
|
||||
// TestForStringRequired ensures that an error is generated if a value is not
|
||||
// provided for a required prompt with no default.
|
||||
func TestForStringRequired(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
var in bytes.Buffer
|
||||
in.Write([]byte("\n")) // [ENTER]
|
||||
|
||||
_ = prompt.ForString("", "",
|
||||
prompt.WithInput(&in),
|
||||
prompt.WithOutput(&out),
|
||||
prompt.WithRequired(true),
|
||||
prompt.WithRetryLimit(1)) // makes the output buffer easier to confirm
|
||||
|
||||
output := out.String()
|
||||
expected := ": \nplease enter a value\n: "
|
||||
if output != expected {
|
||||
t.Fatalf("Unexpected prompt received for a required value. expected '%v', got '%v'", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
// TestForString ensures that string input is accepted.
|
||||
func TestForString(t *testing.T) {
|
||||
var in bytes.Buffer
|
||||
var out bytes.Buffer
|
||||
in.Write([]byte("hunter2\n"))
|
||||
|
||||
s := prompt.ForString("", "",
|
||||
prompt.WithInput(&in),
|
||||
prompt.WithOutput(&out))
|
||||
if s != "hunter2" {
|
||||
t.Fatalf("Expected 'hunter2' got '%v'", s)
|
||||
}
|
||||
}
|
||||
|
||||
// TestForBoolLabel ensures that a prompt for a given boolean prompt prints
|
||||
// the expected y/n prompt.
|
||||
func TestForBoolLabel(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
var in bytes.Buffer
|
||||
in.Write([]byte("\n"))
|
||||
|
||||
// Empty label, default false
|
||||
_ = prompt.ForBool("", false,
|
||||
prompt.WithInput(&in), prompt.WithOutput(&out))
|
||||
if out.String() != "(y/N): " {
|
||||
t.Fatalf("expected output to be '(y/N): ', got '%v'\n", out.String())
|
||||
}
|
||||
|
||||
out.Reset()
|
||||
in.Reset()
|
||||
|
||||
in.Write([]byte("\n"))
|
||||
|
||||
// Empty label, default true
|
||||
_ = prompt.ForBool("", true,
|
||||
prompt.WithInput(&in), prompt.WithOutput(&out))
|
||||
if out.String() != "(Y/n): " {
|
||||
t.Fatalf("expected output to be '(Y/n): ', got '%v'\n", out.String())
|
||||
}
|
||||
|
||||
out.Reset()
|
||||
in.Reset()
|
||||
|
||||
in.Write([]byte("\n"))
|
||||
|
||||
// Populated lablel default false
|
||||
_ = prompt.ForBool("Local", false,
|
||||
prompt.WithInput(&in), prompt.WithOutput(&out))
|
||||
if out.String() != "Local (y/N): " {
|
||||
t.Fatalf("expected 'Local (y/N): ', got '%v'\n", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestForBoolDefault ensures that the default is returned when no user input is given.
|
||||
func TestForBoolDefault(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
var in bytes.Buffer
|
||||
in.Write([]byte("\n"))
|
||||
|
||||
b := prompt.ForBool("", false,
|
||||
prompt.WithInput(&in), prompt.WithOutput(&out))
|
||||
if b != false {
|
||||
t.Fatal("expected default of false to be returned when user accepts.")
|
||||
}
|
||||
|
||||
out.Reset()
|
||||
in.Reset()
|
||||
|
||||
in.Write([]byte("\n"))
|
||||
|
||||
b = prompt.ForBool("", true,
|
||||
prompt.WithInput(&in), prompt.WithOutput(&out))
|
||||
if b != true {
|
||||
t.Fatal("expected default of true to be returned when user accepts.")
|
||||
}
|
||||
}
|
||||
|
||||
// TestForBool ensures that a truthy value, when entered, is returned as a bool.
|
||||
func TestForBool(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
var in bytes.Buffer
|
||||
|
||||
cases := []struct {
|
||||
in string
|
||||
out bool
|
||||
}{
|
||||
{"true", true},
|
||||
{"1", true},
|
||||
{"y", true},
|
||||
{"Y", true},
|
||||
{"yes", true},
|
||||
{"Yes", true},
|
||||
{"YES", true},
|
||||
{"false", false},
|
||||
{"0", false},
|
||||
{"n", false},
|
||||
{"N", false},
|
||||
{"no", false},
|
||||
{"No", false},
|
||||
{"NO", false},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
in.Reset()
|
||||
out.Reset()
|
||||
fmt.Fprintf(&in, "%v\n", c.in)
|
||||
|
||||
// Note the default value is always the oposite of the input
|
||||
// to ensure it is flipped.
|
||||
b := prompt.ForBool("", !c.out,
|
||||
prompt.WithInput(&in), prompt.WithOutput(&out))
|
||||
if b != c.out {
|
||||
t.Fatalf("expected '%v' to be an acceptable %v.", c.in, c.out)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue