diff --git a/.tasks.yml b/.tasks.yml
new file mode 100644
index 00000000..d87975aa
--- /dev/null
+++ b/.tasks.yml
@@ -0,0 +1,10 @@
+tests:
+ summary: Run the test suite
+ command: go test {{.files}}
+
+install-deps:
+ summary: Install all of package dependencies
+ command: go get -t {{.files}}
+
+variables:
+ files: '$(go list -v ./... | grep -Ev "github.com/AlecAivazis/survey/(tests|examples)")'
diff --git a/.travis.yml b/.travis.yml
index e04b93ea..bd8c87bd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,10 @@
language: go
+before_install:
+ - go get github.com/AlecAivazis/run
+
install:
- - go get -t . ./terminal/...
+ - run install-deps
script:
- - go test -v . ./terminal/...
+ - run tests
diff --git a/README.md b/README.md
index 83c7010a..a66caa32 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Survey
[![Build Status](https://travis-ci.org/AlecAivazis/survey.svg?branch=feature%2Fpretty)](https://travis-ci.org/AlecAivazis/survey)
-[![GoDoc](http://img.shields.io/badge/godoc-reference-5272B4.svg)](https://godoc.org/github.com/alecaivazis/survey)
+[![GoDoc](http://img.shields.io/badge/godoc-reference-5272B4.svg)](https://godoc.org/github.com/AlecAivazis/survey)
A library for building interactive prompts. Heavily inspired by the great [inquirer.js](https://github.com/SBoudrias/Inquirer.js/).
@@ -14,7 +14,7 @@ package main
import (
"fmt"
- "gopkg.in/alecaivazis/survey.v0"
+ "gopkg.in/AlecAivazis/survey.v1"
)
// the questions to ask
@@ -26,23 +26,146 @@ var qs = []*survey.Question{
},
{
Name: "color",
- Prompt: &survey.Choice{
+ Prompt: &survey.Select{
Message: "Choose a color:",
- Choices: []string{"red", "blue", "green"},
+ Options: []string{"red", "blue", "green"},
Default: "red",
},
},
}
func main() {
- answers, err := survey.Ask(qs)
+ // the answers will be written to this struct
+ answers := struct {
+ Name string // survey will match the question and field names
+ FavoriteColor string `survey:"color"` // or you can tag fields to match a specific name
+ }{}
+ // perform the questions
+ err := survey.Ask(qs, &answers)
if err != nil {
- fmt.Println("\n", err.Error())
+ fmt.Println(err.Error())
return
}
- fmt.Printf("%s chose %s.", answers["name"], answers["color"])
+ fmt.Printf("%s chose %s.", answers.Name, answers.FavoriteColor)
}
+```
+
+## Examples
+Examples can be found in the `examples/` directory. Run them
+to see basic behavior:
+```bash
+go get github.com/AlecAivazis/survey
+
+# ... navigate to the repo in your GOPATH
+
+go run examples/simple.go
+go run examples/validation.go
+```
+
+## Prompts
+
+### Input
+
+
+```golang
+name := ""
+prompt := &survey.Input{
+ Message: "ping",
+}
+survey.AskOne(prompt, &name, nil)
+```
+
+
+### Password
+
+
+```golang
+password := ""
+prompt := &survey.Password{
+ Message: "Please type your password",
+}
+survey.AskOne(prompt, &password, nil)
+```
+
+
+### Confirm
+
+
+```golang
+name := false
+prompt := &survey.Confirm{
+ Message: "Do you like pie?",
+}
+survey.AskOne(prompt, &name, nil)
+```
+
+
+### Select
+
+
+```golang
+color := ""
+prompt := &survey.Select{
+ Message: "Choose a color:",
+ Options: []string{"red", "blue", "green"},
+}
+survey.AskOne(prompt, &color, nil)
+```
+
+
+### MultiSelect
+
+
+```golang
+days := []string{}
+prompt := &survey.MultiSelect{
+ Message: "What days do you prefer:",
+ Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
+}
+survey.AskOne(prompt, &days, nil)
+```
+
+## Validation
+
+Validating individual responses for a particular question can be done by defining a
+`Validate` field on the `survey.Question` to be validated. This function takes an
+`interface{}` type and returns an error to show to the user, prompting them for another
+response:
+
+```golang
+q := &survey.Question{
+ Prompt: &survey.Input{Message: "Hello world validation"},
+ Validate: func (val interface{}) error {
+ // since we are validating an Input, this will always succeed
+ if str, ok := val.(string) ; ok {
+ if len(str) > 10 {
+ return errors.New("This response cannot be longer than 10 characters.")
+ }
+ }
+ }
+}
+```
+
+### Built-in Validators
+`survey` comes prepackaged with a few validators to fit common situations. Currently these
+validators include:
+
+| name | valid types | description |
+|--------------|-----------------|---------------------------------------------------------------|
+| Required | any | Rejects zero values of the response type |
+| MinLength(n) | string | Enforces that a response is at least the given length |
+| MaxLength(n) | string | Enforces that a response is no longer than the given length |
+
+## Versioning
+
+This project tries to maintain semantic GitHub releases as closely as possible. As such, services
+like [gopkg.in](http://labix.org/gopkg.in) work very well to ensure non-breaking changes whenever
+you build your application. For example, importing v1 of survey would look something like
+
+```golang
+package main
+import "gopkg.in/AlecAivazis/survey.v1"
```
diff --git a/confirm.go b/confirm.go
index 9425f842..786b4898 100644
--- a/confirm.go
+++ b/confirm.go
@@ -4,7 +4,8 @@ import (
"fmt"
"regexp"
- "github.com/alecaivazis/survey/terminal"
+ "github.com/AlecAivazis/survey/core"
+ "github.com/AlecAivazis/survey/terminal"
"github.com/chzyer/readline"
)
@@ -12,7 +13,6 @@ import (
type Confirm struct {
Message string
Default bool
- Answer *bool
}
// data available to the templates when processing
@@ -64,7 +64,7 @@ func (c *Confirm) getBool(rl *readline.Instance) (bool, error) {
answer = c.Default
default:
// we didnt get a valid answer, so print error and prompt again
- out, err := RunTemplate(
+ out, err := core.RunTemplate(
ErrorTemplate, fmt.Errorf("%q is not a valid answer, please try again.", val),
)
// if something went wrong
@@ -88,16 +88,9 @@ func (c *Confirm) getBool(rl *readline.Instance) (bool, error) {
// Prompt prompts the user with a simple text field and expects a reply followed
// by a carriage return.
-func (c *Confirm) Prompt(rl *readline.Instance) (string, error) {
- // if we weren't passed an answer
- if c.Answer == nil {
- // build one
- answer := false
- c.Answer = &answer
- }
-
+func (c *Confirm) Prompt(rl *readline.Instance) (interface{}, error) {
// render the question template
- out, err := RunTemplate(
+ out, err := core.RunTemplate(
ConfirmQuestionTemplate,
ConfirmTemplateData{Confirm: *c},
)
@@ -116,24 +109,30 @@ func (c *Confirm) Prompt(rl *readline.Instance) (string, error) {
return "", err
}
- // return the value
- *c.Answer = answer
-
// convert the boolean into the appropriate string
- return yesNo(answer), nil
+ return answer, nil
}
// Cleanup overwrite the line with the finalized formatted version
-func (c *Confirm) Cleanup(rl *readline.Instance, val string) error {
+func (c *Confirm) Cleanup(rl *readline.Instance, val interface{}) error {
// go up one line
terminal.CursorPreviousLine(1)
// clear the line
- terminal.EraseInLine(1)
+ terminal.EraseInLine(0)
+
+ // the string version of the answer
+ ans := ""
+ // if the value was previously true
+ if val.(bool) {
+ ans = "yes"
+ } else {
+ ans = "no"
+ }
// render the template
- out, err := RunTemplate(
+ out, err := core.RunTemplate(
ConfirmQuestionTemplate,
- ConfirmTemplateData{Confirm: *c, Answer: val},
+ ConfirmTemplateData{Confirm: *c, Answer: ans},
)
if err != nil {
return err
diff --git a/confirm_test.go b/confirm_test.go
index ad730fb9..c117d0bf 100644
--- a/confirm_test.go
+++ b/confirm_test.go
@@ -2,11 +2,13 @@ package survey
import (
"testing"
+
+ "github.com/AlecAivazis/survey/core"
)
func init() {
// disable color output for all prompts to simplify testing
- DisableColor = true
+ core.DisableColor = true
}
func TestConfirmFormatQuestion(t *testing.T) {
@@ -16,7 +18,7 @@ func TestConfirmFormatQuestion(t *testing.T) {
Default: true,
}
- actual, err := RunTemplate(
+ actual, err := core.RunTemplate(
ConfirmQuestionTemplate,
ConfirmTemplateData{Confirm: *prompt},
)
@@ -38,7 +40,7 @@ func TestConfirmFormatQuestionDefaultFalse(t *testing.T) {
Default: false,
}
- actual, err := RunTemplate(
+ actual, err := core.RunTemplate(
ConfirmQuestionTemplate,
ConfirmTemplateData{Confirm: *prompt},
)
@@ -60,7 +62,7 @@ func TestConfirmFormatAnswer(t *testing.T) {
Message: "Is pizza your favorite food?",
}
- actual, err := RunTemplate(
+ actual, err := core.RunTemplate(
ConfirmQuestionTemplate,
ConfirmTemplateData{Confirm: *prompt, Answer: "Yes"},
)
diff --git a/template.go b/core/template.go
similarity index 98%
rename from template.go
rename to core/template.go
index 88ce8a2f..c2716cc0 100644
--- a/template.go
+++ b/core/template.go
@@ -1,4 +1,4 @@
-package survey
+package core
import (
"bytes"
diff --git a/core/write.go b/core/write.go
new file mode 100644
index 00000000..6c3c862e
--- /dev/null
+++ b/core/write.go
@@ -0,0 +1,102 @@
+package core
+
+import (
+ "errors"
+ "fmt"
+ "reflect"
+ "strings"
+)
+
+// the tag used to denote the name of the question
+const tagName = "survey"
+
+func WriteAnswer(t interface{}, name string, v interface{}) (err error) {
+ // the target to write to
+ target := reflect.ValueOf(t)
+ // the value to write from
+ value := reflect.ValueOf(v)
+
+ // make sure we are writing to a pointer
+ if target.Kind() != reflect.Ptr {
+ return errors.New("you must pass a pointer as the target of a Write operation")
+ }
+ // the object "inside" of the target pointer
+ elem := target.Elem()
+
+ // handle the special types
+ switch elem.Kind() {
+ // if we are writing to a struct
+ case reflect.Struct:
+ // get the name of the field that matches the string we were given
+ fieldIndex, err := findFieldIndex(elem, name)
+ // if something went wrong
+ if err != nil {
+ // bubble up
+ return err
+ }
+
+ // copy the value over to the field
+ return copy(elem.Field(fieldIndex), value)
+ }
+
+ // otherwise just copy the value to the target
+ return copy(elem, value)
+}
+
+// BUG(AlecAivazis): the current implementation might cause weird conflicts if there are
+// two fields with same name that only differ by casing.
+func findFieldIndex(s reflect.Value, name string) (int, error) {
+ // the type of the value
+ sType := s.Type()
+
+ // first look for matching tags so we can overwrite matching field names
+ for i := 0; i < sType.NumField(); i++ {
+ // the field we are current scanning
+ field := sType.Field(i)
+
+ // the value of the survey tag
+ tag := field.Tag.Get(tagName)
+ // if the tag matches the name we are looking for
+ if tag != "" && tag == name {
+ // then we found our index
+ return i, nil
+ }
+ }
+
+ // then look for matching names
+ for i := 0; i < sType.NumField(); i++ {
+ // the field we are current scanning
+ field := sType.Field(i)
+
+ // if the name of the field matches what we're looking for
+ if strings.ToLower(field.Name) == strings.ToLower(name) {
+ return i, nil
+ }
+ }
+
+ // we didn't find the field
+ return -1, fmt.Errorf("could not find field matching %v", name)
+}
+
+// Write takes a value and copies it to the target
+func copy(t reflect.Value, v reflect.Value) (err error) {
+ // if something ends up panicing we need to catch it in a deferred func
+ defer func() {
+ if r := recover(); r != nil {
+ // if we paniced with an error
+ if _, ok := r.(error); ok {
+ // cast the result to an error object
+ err = r.(error)
+ } else if _, ok := r.(string); ok {
+ // otherwise we could have paniced with a string so wrap it in an error
+ err = errors.New(r.(string))
+ }
+ }
+ }()
+
+ // attempt to copy the underlying value to the target
+ t.Set(v)
+
+ // we're done
+ return
+}
diff --git a/core/write_test.go b/core/write_test.go
new file mode 100644
index 00000000..a410df3a
--- /dev/null
+++ b/core/write_test.go
@@ -0,0 +1,219 @@
+package core
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestWrite_returnsErrorIfTargetNotPtr(t *testing.T) {
+ // try to copy a value to a non-pointer
+ err := WriteAnswer(true, "hello", true)
+ // make sure there was an error
+ if err == nil {
+ t.Error("Did not encounter error when writing to non-pointer.")
+ }
+}
+
+func TestWrite_canWriteToBool(t *testing.T) {
+ // a pointer to hold the boolean value
+ ptr := true
+
+ // try to copy a false value to the pointer
+ WriteAnswer(&ptr, "", false)
+
+ // if the value is true
+ if ptr {
+ // the test failed
+ t.Error("Could not write a false bool to a pointer")
+ }
+}
+
+func TestWrite_canWriteString(t *testing.T) {
+ // a pointer to hold the boolean value
+ ptr := ""
+
+ // try to copy a false value to the pointer
+ err := WriteAnswer(&ptr, "", "hello")
+ if err != nil {
+ t.Error(err)
+ }
+
+ // if the value is not what we wrote
+ if ptr != "hello" {
+ t.Error("Could not write a string value to a pointer")
+ }
+}
+
+func TestWrite_canWriteSlice(t *testing.T) {
+ // a pointer to hold the value
+ ptr := []string{}
+
+ // copy in a value
+ WriteAnswer(&ptr, "", []string{"hello", "world"})
+
+ // make sure there are two entries
+ if len(ptr) != 2 {
+ // the test failed
+ t.Errorf("Incorrect number of entries in written list. Expected 2, found %v.", len(ptr))
+ // dont move on
+ return
+ }
+
+ // make sure the first entry is hello
+ if ptr[0] != "hello" {
+ // the test failed
+ t.Errorf("incorrect first value in written pointer. expected hello found %v.", ptr[0])
+ }
+
+ // make sure the second entry is world
+ if ptr[1] != "world" {
+ // the test failed
+ t.Errorf("incorrect second value in written pointer. expected world found %v.", ptr[0])
+ }
+}
+
+func TestWrite_recoversInvalidReflection(t *testing.T) {
+ // a variable to mutate
+ ptr := false
+
+ // write a boolean value to the string
+ err := WriteAnswer(&ptr, "", "hello")
+
+ // if there was no error
+ if err == nil {
+ // the test failed
+ t.Error("Did not encounter error when forced invalid write.")
+ }
+}
+
+func TestWriteAnswer_handlesNonStructValues(t *testing.T) {
+ // the value to write to
+ ptr := ""
+
+ // write a value to the pointer
+ WriteAnswer(&ptr, "", "world")
+
+ // if we didn't change the value appropriate
+ if ptr != "world" {
+ // the test failed
+ t.Error("Did not write value to primitive pointer")
+ }
+}
+
+func TestWriteAnswer_canMutateStruct(t *testing.T) {
+ // the struct to hold the answer
+ ptr := struct{ Name string }{}
+
+ // write a value to an existing field
+ err := WriteAnswer(&ptr, "name", "world")
+ if err != nil {
+ // the test failed
+ t.Errorf("Encountered error while writing answer: %v", err.Error())
+ // we're done here
+ return
+ }
+
+ // make sure we changed the field
+ if ptr.Name != "world" {
+ // the test failed
+ t.Error("Did not mutate struct field when writing answer.")
+ }
+}
+
+func TestWriteAnswer_returnsErrWhenFieldNotFound(t *testing.T) {
+ // the struct to hold the answer
+ ptr := struct{ Name string }{}
+
+ // write a value to an existing field
+ err := WriteAnswer(&ptr, "", "world")
+
+ if err == nil {
+ // the test failed
+ t.Error("Did not encountered error while writing answer to non-existing field.")
+ }
+}
+
+func TestFindFieldIndex_canFindExportedField(t *testing.T) {
+ // create a reflective wrapper over the struct to look through
+ val := reflect.ValueOf(struct{ Name string }{})
+
+ // find the field matching "name"
+ fieldIndex, err := findFieldIndex(val, "name")
+ // if something went wrong
+ if err != nil {
+ // the test failed
+ t.Error(err.Error())
+ return
+ }
+
+ // make sure we got the right value
+ if val.Type().Field(fieldIndex).Name != "Name" {
+ // the test failed
+ t.Errorf("Did not find the correct field name. Expected 'Name' found %v.", val.Type().Field(fieldIndex).Name)
+ }
+}
+
+func TestFindFieldIndex_canFindTaggedField(t *testing.T) {
+ // the struct to look through
+ val := reflect.ValueOf(struct {
+ Username string `survey:"name"`
+ }{})
+
+ // find the field matching "name"
+ fieldIndex, err := findFieldIndex(val, "name")
+ // if something went wrong
+ if err != nil {
+ // the test failed
+ t.Error(err.Error())
+ return
+ }
+
+ // make sure we got the right value
+ if val.Type().Field(fieldIndex).Name != "Username" {
+ // the test failed
+ t.Errorf("Did not find the correct field name. Expected 'Username' found %v.", val.Type().Field(fieldIndex).Name)
+ }
+}
+
+func TestFindFieldIndex_canHandleCapitalAnswerNames(t *testing.T) {
+ // create a reflective wrapper over the struct to look through
+ val := reflect.ValueOf(struct{ Name string }{})
+
+ // find the field matching "name"
+ fieldIndex, err := findFieldIndex(val, "Name")
+ // if something went wrong
+ if err != nil {
+ // the test failed
+ t.Error(err.Error())
+ return
+ }
+
+ // make sure we got the right value
+ if val.Type().Field(fieldIndex).Name != "Name" {
+ // the test failed
+ t.Errorf("Did not find the correct field name. Expected 'Name' found %v.", val.Type().Field(fieldIndex).Name)
+ }
+}
+
+func TestFindFieldIndex_tagOverwriteFieldName(t *testing.T) {
+ // the struct to look through
+ val := reflect.ValueOf(struct {
+ Name string
+ Username string `survey:"name"`
+ }{})
+
+ // find the field matching "name"
+ fieldIndex, err := findFieldIndex(val, "name")
+ // if something went wrong
+ if err != nil {
+ // the test failed
+ t.Error(err.Error())
+ return
+ }
+
+ // make sure we got the right value
+ if val.Type().Field(fieldIndex).Name != "Username" {
+ // the test failed
+ t.Errorf("Did not find the correct field name. Expected 'Username' found %v.", val.Type().Field(fieldIndex).Name)
+ }
+}
diff --git a/examples/confirm.go b/examples/confirm.go
deleted file mode 100644
index d90ca8d8..00000000
--- a/examples/confirm.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package main
-
-import (
- "fmt"
-
- "github.com/alecaivazis/survey"
-)
-
-func main() {
- prompt := &survey.Confirm{
- Message: "Are you happy?",
- }
-
- answer, err := survey.AskOne(prompt)
-
- if err != nil {
- fmt.Println(err.Error())
- return
- }
-
- fmt.Printf("response string: %s\n", answer)
-}
diff --git a/examples/multichoice.go b/examples/multichoice.go
deleted file mode 100644
index 17ba0f01..00000000
--- a/examples/multichoice.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package main
-
-import (
- "fmt"
-
- "github.com/alecaivazis/survey"
-)
-
-func main() {
- days := []string{}
- prompt := &survey.MultiChoice{
- Message: "What days do you prefer:",
- Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
- Defaults: []string{"Saturday", "Sunday"},
- Answer: &days,
- }
-
- answer, err := survey.AskOne(prompt)
-
- if err != nil {
- fmt.Println(err.Error())
- return
- }
-
- fmt.Printf("response string (json for MultiChoice): %s\n", answer)
- fmt.Printf("response days: %#v\n", days)
-}
diff --git a/examples/simple.go b/examples/simple.go
index 6fd98b3d..c3723fa5 100644
--- a/examples/simple.go
+++ b/examples/simple.go
@@ -3,7 +3,7 @@ package main
import (
"fmt"
- "github.com/alecaivazis/survey"
+ "github.com/AlecAivazis/survey"
)
// the questions to ask
@@ -17,22 +17,26 @@ var simpleQs = []*survey.Question{
},
{
Name: "color",
- Prompt: &survey.Choice{
+ Prompt: &survey.Select{
Message: "Choose a color:",
- Choices: []string{"red", "blue", "green"},
+ Options: []string{"red", "blue", "green"},
},
Validate: survey.Required,
},
}
func main() {
+ answers := struct {
+ Name string
+ Color string
+ }{}
// ask the question
- answers, err := survey.Ask(simpleQs)
+ err := survey.Ask(simpleQs, &answers)
if err != nil {
fmt.Println(err.Error())
return
}
// print the answers
- fmt.Printf("%s chose %s", answers["name"], answers["color"])
+ fmt.Printf("%s chose %s.\n", answers.Name, answers.Color)
}
diff --git a/examples/validation.go b/examples/validation.go
index bc0b758c..396aa0cb 100644
--- a/examples/validation.go
+++ b/examples/validation.go
@@ -1,9 +1,9 @@
package main
import (
- "errors"
"fmt"
- "github.com/alecaivazis/survey"
+
+ "github.com/AlecAivazis/survey"
)
// the questions to ask
@@ -16,10 +16,10 @@ var validationQs = []*survey.Question{
{
Name: "valid",
Prompt: &survey.Input{"Enter 'foo':", "not foo"},
- Validate: func(str string) error {
+ Validate: func(val interface{}) error {
// if the input matches the expectation
- if str != "foo" {
- return errors.New(fmt.Sprintf("You entered %s, not 'foo'.", str))
+ if str := val.(string); str != "foo" {
+ return fmt.Errorf("You entered %s, not 'foo'.", str)
}
// nothing was wrong
return nil
@@ -28,8 +28,12 @@ var validationQs = []*survey.Question{
}
func main() {
-
- _, err := survey.Ask(validationQs)
+ // the place to hold the answers
+ answers := struct {
+ Name string
+ Valid string
+ }{}
+ err := survey.Ask(validationQs, &answers)
if err != nil {
fmt.Println("\n", err.Error())
diff --git a/input.go b/input.go
index fcf0cbf0..3ca884db 100644
--- a/input.go
+++ b/input.go
@@ -3,7 +3,8 @@ package survey
import (
"fmt"
- "github.com/alecaivazis/survey/terminal"
+ "github.com/AlecAivazis/survey/core"
+ "github.com/AlecAivazis/survey/terminal"
"github.com/chzyer/readline"
)
@@ -30,9 +31,9 @@ var InputQuestionTemplate = `
{{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
{{- end}}`
-func (i *Input) Prompt(rl *readline.Instance) (line string, err error) {
+func (i *Input) Prompt(rl *readline.Instance) (line interface{}, err error) {
// render the template
- out, err := RunTemplate(
+ out, err := core.RunTemplate(
InputQuestionTemplate,
InputTemplateData{Input: *i},
)
@@ -47,16 +48,16 @@ func (i *Input) Prompt(rl *readline.Instance) (line string, err error) {
return line, err
}
-func (i *Input) Cleanup(rl *readline.Instance, val string) error {
+func (i *Input) Cleanup(rl *readline.Instance, val interface{}) error {
// go up one line
terminal.CursorPreviousLine(1)
// clear the line
- terminal.EraseInLine(1)
+ terminal.EraseInLine(0)
// render the template
- out, err := RunTemplate(
+ out, err := core.RunTemplate(
InputQuestionTemplate,
- InputTemplateData{Input: *i, Answer: val},
+ InputTemplateData{Input: *i, Answer: val.(string)},
)
if err != nil {
return err
diff --git a/input_test.go b/input_test.go
index 3d680a6a..32e372f7 100644
--- a/input_test.go
+++ b/input_test.go
@@ -2,11 +2,13 @@ package survey
import (
"testing"
+
+ "github.com/AlecAivazis/survey/core"
)
func init() {
// disable color output for all prompts to simplify testing
- DisableColor = true
+ core.DisableColor = true
}
func TestInputFormatQuestion(t *testing.T) {
@@ -16,7 +18,7 @@ func TestInputFormatQuestion(t *testing.T) {
Default: "April",
}
- actual, err := RunTemplate(
+ actual, err := core.RunTemplate(
InputQuestionTemplate,
InputTemplateData{Input: *prompt},
)
@@ -38,7 +40,7 @@ func TestInputFormatAnswer(t *testing.T) {
Default: "April",
}
- actual, err := RunTemplate(
+ actual, err := core.RunTemplate(
InputQuestionTemplate,
InputTemplateData{Input: *prompt, Answer: "October"},
)
diff --git a/multichoice_test.go b/multichoice_test.go
deleted file mode 100644
index 41c38d9c..00000000
--- a/multichoice_test.go
+++ /dev/null
@@ -1,85 +0,0 @@
-package survey
-
-import "testing"
-
-func init() {
- // disable color output for all prompts to simplify testing
- DisableColor = true
-}
-
-func TestCanFormatMultiChoiceOptions(t *testing.T) {
-
- prompt := &MultiChoice{
- Options: []string{"foo", "bar", "baz", "buz"},
- Defaults: []string{"bar", "buz"},
- }
-
- actual, err := RunTemplate(
- MultiChoiceOptionsTemplate,
- MultiChoiceTemplateData{
- MultiChoice: *prompt,
- SelectedIndex: 2,
- Checked: map[int]bool{1: true, 3: true},
- },
- )
-
- if err != nil {
- t.Errorf("Failed to run template to format checkbox options: %s", err)
- }
-
- expected := ` ◯ foo
- ◉ bar
-❯ ◯ baz
- ◉ buz
-`
-
- if actual != expected {
- t.Errorf("Formatted checkbox options were not formatted correctly. Found:\n%s\nExpected:\n%s", actual, expected)
- }
-}
-
-func TestMultiChoiceFormatQuestion(t *testing.T) {
-
- prompt := &MultiChoice{
- Message: "Pick your words:",
- Options: []string{"foo", "bar", "baz", "buz"},
- Defaults: []string{"bar", "buz"},
- }
-
- actual, err := RunTemplate(
- MultiChoiceQuestionTemplate,
- MultiChoiceTemplateData{MultiChoice: *prompt},
- )
- if err != nil {
- t.Errorf("Failed to run template to format checkbox question: %s", err)
- }
-
- expected := `? Pick your words: `
-
- if actual != expected {
- t.Errorf("Formatted checkbox question was not formatted correctly. Found:\n%s\nExpected:\n%s", actual, expected)
- }
-}
-
-func TestMultiChoiceFormatAnswer(t *testing.T) {
-
- prompt := &MultiChoice{
- Message: "Pick your words:",
- Options: []string{"foo", "bar", "baz", "buz"},
- Defaults: []string{"bar", "buz"},
- }
-
- actual, err := RunTemplate(
- MultiChoiceQuestionTemplate,
- MultiChoiceTemplateData{MultiChoice: *prompt, Answer: []string{"foo", "buz"}},
- )
- if err != nil {
- t.Errorf("Failed to run template to format checkbox answer: %s", err)
- }
-
- expected := `? Pick your words: ["foo" "buz"]`
-
- if actual != expected {
- t.Errorf("Formatted checkbox answer was not formatted correctly. Found:\n%s\nExpected:\n%s", actual, expected)
- }
-}
diff --git a/multichoice.go b/multiselect.go
similarity index 66%
rename from multichoice.go
rename to multiselect.go
index 3310baf9..7a0ff0e4 100644
--- a/multichoice.go
+++ b/multiselect.go
@@ -1,40 +1,39 @@
package survey
import (
- "encoding/json"
"errors"
"io/ioutil"
"strings"
- "github.com/alecaivazis/survey/terminal"
+ "github.com/AlecAivazis/survey/core"
+ "github.com/AlecAivazis/survey/terminal"
"github.com/chzyer/readline"
)
-// MultiChoice is a prompt that presents a list of various options to the user
+// MultiSelect is a prompt that presents a list of various options to the user
// for them to select using the arrow keys and enter.
-type MultiChoice struct {
+type MultiSelect struct {
Message string
Options []string
- Defaults []string
- Answer *[]string
+ Default []string
selectedIndex int
checked map[int]bool
}
// data available to the templates when processing
-type MultiChoiceTemplateData struct {
- MultiChoice
- Answer []string
+type MultiSelectTemplateData struct {
+ MultiSelect
+ Answer string
Checked map[int]bool
SelectedIndex int
}
-var MultiChoiceQuestionTemplate = `
+var MultiSelectQuestionTemplate = `
{{- color "green+hb"}}? {{color "reset"}}
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
-{{- if .Answer}}{{color "cyan"}}{{.Answer | printf "%q"}}{{color "reset"}}{{end}}`
+{{- if .Answer}}{{color "cyan"}}{{.Answer}}{{color "reset"}}{{end}}`
-var MultiChoiceOptionsTemplate = `
+var MultiSelectOptionsTemplate = `
{{- range $ix, $option := .Options}}
{{- if eq $ix $.SelectedIndex}}{{color "cyan"}}❯{{color "reset"}}{{else}} {{end}}
{{- if index $.Checked $ix}}{{color "green"}} ◉ {{else}}{{color "default+hb"}} ◯ {{end}}
@@ -43,7 +42,7 @@ var MultiChoiceOptionsTemplate = `
{{end}}`
// OnChange is called on every keypress.
-func (m *MultiChoice) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
+func (m *MultiSelect) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
if key == terminal.KeyEnter {
// just pass on the current value
return line, 0, true
@@ -72,18 +71,18 @@ func (m *MultiChoice) OnChange(line []rune, pos int, key rune) (newLine []rune,
return line, 0, true
}
-func (m *MultiChoice) render() error {
+func (m *MultiSelect) render() error {
// clean up what we left behind last time
for range m.Options {
terminal.CursorPreviousLine(1)
- terminal.EraseInLine(1)
+ terminal.EraseInLine(0)
}
// render the template summarizing the current state
- out, err := RunTemplate(
- MultiChoiceOptionsTemplate,
- MultiChoiceTemplateData{
- MultiChoice: *m,
+ out, err := core.RunTemplate(
+ MultiSelectOptionsTemplate,
+ MultiSelectTemplateData{
+ MultiSelect: *m,
SelectedIndex: m.selectedIndex,
Checked: m.checked,
},
@@ -99,14 +98,7 @@ func (m *MultiChoice) render() error {
return nil
}
-func (m *MultiChoice) Prompt(rl *readline.Instance) (string, error) {
- // if the user didn't pass an answer reference
- if m.Answer == nil {
- // build one
- answer := []string{}
- m.Answer = &answer
- }
-
+func (m *MultiSelect) Prompt(rl *readline.Instance) (interface{}, error) {
// the readline config
config := &readline.Config{
Listener: m,
@@ -117,8 +109,8 @@ func (m *MultiChoice) Prompt(rl *readline.Instance) (string, error) {
// compute the default state
m.checked = make(map[int]bool)
// if there is a default
- if len(m.Defaults) > 0 {
- for _, dflt := range m.Defaults {
+ if len(m.Default) > 0 {
+ for _, dflt := range m.Default {
for i, opt := range m.Options {
// if the option correponds to the default
if opt == dflt {
@@ -137,10 +129,10 @@ func (m *MultiChoice) Prompt(rl *readline.Instance) (string, error) {
return "", errors.New("please provide options to select from")
}
// generate the template for the current state of the prompt
- out, err := RunTemplate(
- MultiChoiceQuestionTemplate,
- MultiChoiceTemplateData{
- MultiChoice: *m,
+ out, err := core.RunTemplate(
+ MultiSelectQuestionTemplate,
+ MultiSelectTemplateData{
+ MultiSelect: *m,
SelectedIndex: m.selectedIndex,
Checked: m.checked,
},
@@ -171,48 +163,27 @@ func (m *MultiChoice) Prompt(rl *readline.Instance) (string, error) {
answers = append(answers, option)
}
}
- *m.Answer = answers
- // nothing went wrong
- return m.value()
-}
-
-func (m *MultiChoice) value() (string, error) {
- answers := []string{}
- for ix, option := range m.Options {
- if val, ok := m.checked[ix]; ok && val {
- answers = append(answers, option)
- }
- }
- // return the selected option
- js, err := json.Marshal(answers)
- if err != nil {
- return "", err
- }
- return string(js), nil
+ return answers, nil
}
// Cleanup removes the options section, and renders the ask like a normal question.
-func (m *MultiChoice) Cleanup(rl *readline.Instance, val string) error {
+func (m *MultiSelect) Cleanup(rl *readline.Instance, val interface{}) error {
terminal.CursorPreviousLine(1)
- terminal.EraseInLine(1)
+ terminal.EraseInLine(0)
for range m.Options {
terminal.CursorPreviousLine(1)
- terminal.EraseInLine(1)
+ terminal.EraseInLine(0)
}
- // parse the value into a list of strings
- var value []string
- json.Unmarshal([]byte(val), &value)
-
// execute the output summary template with the answer
- output, err := RunTemplate(
- MultiChoiceQuestionTemplate,
- MultiChoiceTemplateData{
- MultiChoice: *m,
+ output, err := core.RunTemplate(
+ MultiSelectQuestionTemplate,
+ MultiSelectTemplateData{
+ MultiSelect: *m,
SelectedIndex: m.selectedIndex,
Checked: m.checked,
- Answer: value,
+ Answer: strings.Join(val.([]string), ", "),
},
)
if err != nil {
diff --git a/multiselect_test.go b/multiselect_test.go
new file mode 100644
index 00000000..ce53a510
--- /dev/null
+++ b/multiselect_test.go
@@ -0,0 +1,90 @@
+package survey
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/AlecAivazis/survey/core"
+)
+
+func init() {
+ // disable color output for all prompts to simplify testing
+ core.DisableColor = true
+}
+
+func TestCanFormatMultiSelectOptions(t *testing.T) {
+
+ prompt := &MultiSelect{
+ Options: []string{"foo", "bar", "baz", "buz"},
+ Default: []string{"bar", "buz"},
+ }
+
+ actual, err := core.RunTemplate(
+ MultiSelectOptionsTemplate,
+ MultiSelectTemplateData{
+ MultiSelect: *prompt,
+ SelectedIndex: 2,
+ Checked: map[int]bool{1: true, 3: true},
+ },
+ )
+
+ if err != nil {
+ t.Errorf("Failed to run template to format checkbox options: %s", err)
+ }
+
+ expected := ` ◯ foo
+ ◉ bar
+❯ ◯ baz
+ ◉ buz
+`
+
+ if actual != expected {
+ t.Errorf("Formatted checkbox options were not formatted correctly. Found:\n%s\nExpected:\n%s", actual, expected)
+ }
+}
+
+func TestMultiSelectFormatQuestion(t *testing.T) {
+
+ prompt := &MultiSelect{
+ Message: "Pick your words:",
+ Options: []string{"foo", "bar", "baz", "buz"},
+ Default: []string{"bar", "buz"},
+ }
+
+ actual, err := core.RunTemplate(
+ MultiSelectQuestionTemplate,
+ MultiSelectTemplateData{MultiSelect: *prompt},
+ )
+ if err != nil {
+ t.Errorf("Failed to run template to format checkbox question: %s", err)
+ }
+
+ expected := `? Pick your words: `
+
+ if actual != expected {
+ t.Errorf("Formatted checkbox question was not formatted correctly. Found:\n%s\nExpected:\n%s", actual, expected)
+ }
+}
+
+func TestMultiSelectFormatAnswer(t *testing.T) {
+
+ prompt := &MultiSelect{
+ Message: "Pick your words:",
+ Options: []string{"foo", "bar", "baz", "buz"},
+ Default: []string{"bar", "buz"},
+ }
+
+ actual, err := core.RunTemplate(
+ MultiSelectQuestionTemplate,
+ MultiSelectTemplateData{MultiSelect: *prompt, Answer: strings.Join([]string{"foo", "buz"}, ", ")},
+ )
+ if err != nil {
+ t.Errorf("Failed to run template to format checkbox answer: %s", err)
+ }
+
+ expected := `? Pick your words: foo, buz`
+
+ if actual != expected {
+ t.Errorf("Formatted checkbox answer was not formatted correctly. Found:\n%s\nExpected:\n%s", actual, expected)
+ }
+}
diff --git a/password.go b/password.go
index 1c9f26c0..08531aa5 100644
--- a/password.go
+++ b/password.go
@@ -2,6 +2,8 @@ package survey
import (
"github.com/chzyer/readline"
+
+ "github.com/AlecAivazis/survey/core"
)
// Password is like a normal Input but the text shows up as *'s and
@@ -15,9 +17,9 @@ var PasswordQuestionTemplate = `
{{- color "green+hb"}}? {{color "reset"}}
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}`
-func (p *Password) Prompt(rl *readline.Instance) (line string, err error) {
+func (p *Password) Prompt(rl *readline.Instance) (line interface{}, err error) {
// render the question template
- out, err := RunTemplate(
+ out, err := core.RunTemplate(
PasswordQuestionTemplate,
*p,
)
@@ -39,6 +41,6 @@ func (p *Password) Prompt(rl *readline.Instance) (line string, err error) {
}
// Cleanup hides the string with a fixed number of characters.
-func (prompt *Password) Cleanup(rl *readline.Instance, val string) error {
+func (prompt *Password) Cleanup(rl *readline.Instance, val interface{}) error {
return nil
}
diff --git a/password_test.go b/password_test.go
index 3fd7b7c3..3d1d049d 100644
--- a/password_test.go
+++ b/password_test.go
@@ -2,11 +2,13 @@ package survey
import (
"testing"
+
+ "github.com/AlecAivazis/survey/core"
)
func init() {
// disable color output for all prompts to simplify testing
- DisableColor = true
+ core.DisableColor = true
}
func TestPasswordFormatQuestion(t *testing.T) {
@@ -15,7 +17,7 @@ func TestPasswordFormatQuestion(t *testing.T) {
Message: "Tell me your secret:",
}
- actual, err := RunTemplate(
+ actual, err := core.RunTemplate(
PasswordQuestionTemplate,
*prompt,
)
diff --git a/choice.go b/select.go
similarity index 63%
rename from choice.go
rename to select.go
index 2a5f2e96..109a9df7 100644
--- a/choice.go
+++ b/select.go
@@ -5,71 +5,73 @@ import (
"io/ioutil"
"strings"
- "github.com/alecaivazis/survey/terminal"
+ "github.com/AlecAivazis/survey/core"
+ "github.com/AlecAivazis/survey/terminal"
"github.com/chzyer/readline"
)
-// Choice is a prompt that presents a list of various options to the user
+// Select is a prompt that presents a list of various options to the user
// for them to select using the arrow keys and enter.
-type Choice struct {
+type Select struct {
Message string
- Choices []string
+ Options []string
Default string
- SelectedIndex int
+ selectedIndex int
}
// the data available to the templates when processing
type SelectTemplateData struct {
- Select Choice
- Answer string
+ Select
+ SelectedIndex int
+ Answer string
}
const (
SelectQuestionTemplate = `
{{- color "green+hb"}}? {{color "reset"}}
-{{- color "default+hb"}}{{ $.Select.Message }} {{color "reset"}}
+{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
{{- if .Answer}}{{color "cyan"}}{{.Answer}}{{color "reset"}}{{end}}`
// the template used to show the list of Selects
SelectChoicesTemplate = `
-{{- range $ix, $choice := $.Select.Choices}}
- {{- if eq $ix $.Select.SelectedIndex}}{{color "cyan+b"}}> {{else}}{{color "default+hb"}} {{end}}
+{{- range $ix, $choice := .Options}}
+ {{- if eq $ix $.SelectedIndex}}{{color "cyan+b"}}> {{else}}{{color "default+hb"}} {{end}}
{{- $choice}}
{{- color "reset"}}
{{end}}`
)
// OnChange is called on every keypress.
-func (s *Choice) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
+func (s *Select) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
// if the user pressed the enter key
if key == terminal.KeyEnter {
- return []rune(s.Choices[s.SelectedIndex]), 0, true
+ return []rune(s.Options[s.selectedIndex]), 0, true
// if the user pressed the up arrow
- } else if key == terminal.KeyArrowUp && s.SelectedIndex > 0 {
+ } else if key == terminal.KeyArrowUp && s.selectedIndex > 0 {
// decrement the selected index
- s.SelectedIndex--
+ s.selectedIndex--
// if the user pressed down and there is room to move
- } else if key == terminal.KeyArrowDown && s.SelectedIndex < len(s.Choices)-1 {
+ } else if key == terminal.KeyArrowDown && s.selectedIndex < len(s.Options)-1 {
// increment the selected index
- s.SelectedIndex++
+ s.selectedIndex++
}
// render the options
s.render()
// if we are not pressing ent
- return []rune(s.Choices[s.SelectedIndex]), 0, true
+ return []rune(s.Options[s.selectedIndex]), 0, true
}
-func (s *Choice) render() error {
- for range s.Choices {
+func (s *Select) render() error {
+ for range s.Options {
terminal.CursorPreviousLine(1)
- terminal.EraseInLine(1)
+ terminal.EraseInLine(0)
}
// the formatted response
- out, err := RunTemplate(
+ out, err := core.RunTemplate(
SelectChoicesTemplate,
- SelectTemplateData{Select: *s},
+ SelectTemplateData{Select: *s, SelectedIndex: s.selectedIndex},
)
if err != nil {
return err
@@ -81,7 +83,7 @@ func (s *Choice) render() error {
return nil
}
-func (s *Choice) Prompt(rl *readline.Instance) (string, error) {
+func (s *Select) Prompt(rl *readline.Instance) (interface{}, error) {
config := &readline.Config{
Listener: s,
Stdout: ioutil.Discard,
@@ -89,7 +91,7 @@ func (s *Choice) Prompt(rl *readline.Instance) (string, error) {
rl.SetConfig(config)
// if there are no options to render
- if len(s.Choices) == 0 {
+ if len(s.Options) == 0 {
// we failed
return "", errors.New("please provide options to select from")
}
@@ -99,7 +101,7 @@ func (s *Choice) Prompt(rl *readline.Instance) (string, error) {
// if there is a default
if s.Default != "" {
// find the choice
- for i, opt := range s.Choices {
+ for i, opt := range s.Options {
// if the option correponds to the default
if opt == s.Default {
// we found our initial value
@@ -110,12 +112,12 @@ func (s *Choice) Prompt(rl *readline.Instance) (string, error) {
}
}
// save the selected index
- s.SelectedIndex = sel
+ s.selectedIndex = sel
// render the initial question
- out, err := RunTemplate(
+ out, err := core.RunTemplate(
SelectQuestionTemplate,
- SelectTemplateData{Select: *s},
+ SelectTemplateData{Select: *s, SelectedIndex: sel},
)
if err != nil {
return "", err
@@ -125,7 +127,7 @@ func (s *Choice) Prompt(rl *readline.Instance) (string, error) {
terminal.CursorHide()
// ask the question
terminal.Println(out)
- for range s.Choices {
+ for range s.Options {
terminal.Println()
}
// start waiting for input
@@ -141,7 +143,7 @@ func (s *Choice) Prompt(rl *readline.Instance) (string, error) {
val = s.Default
} else {
// there is no default value so use the first
- val = s.Choices[0]
+ val = s.Options[0]
}
}
@@ -149,18 +151,18 @@ func (s *Choice) Prompt(rl *readline.Instance) (string, error) {
return val, err
}
-func (s *Choice) Cleanup(rl *readline.Instance, val string) error {
+func (s *Select) Cleanup(rl *readline.Instance, val interface{}) error {
terminal.CursorPreviousLine(1)
- terminal.EraseInLine(1)
- for range s.Choices {
+ terminal.EraseInLine(0)
+ for range s.Options {
terminal.CursorPreviousLine(1)
- terminal.EraseInLine(1)
+ terminal.EraseInLine(0)
}
// execute the output summary template with the answer
- output, err := RunTemplate(
+ output, err := core.RunTemplate(
SelectQuestionTemplate,
- SelectTemplateData{Select: *s, Answer: val},
+ SelectTemplateData{Select: *s, Answer: val.(string)},
)
if err != nil {
return err
diff --git a/choice_test.go b/select_test.go
similarity index 72%
rename from choice_test.go
rename to select_test.go
index d4478c91..861f0045 100644
--- a/choice_test.go
+++ b/select_test.go
@@ -2,29 +2,31 @@ package survey
import (
"testing"
+
+ "github.com/AlecAivazis/survey/core"
)
func init() {
// disable color output for all prompts to simplify testing
- DisableColor = true
+ core.DisableColor = true
}
func TestCanFormatSelectOptions(t *testing.T) {
- prompt := &Choice{
- Choices: []string{"foo", "bar", "baz", "buz"},
+ prompt := &Select{
+ Options: []string{"foo", "bar", "baz", "buz"},
Default: "baz",
}
// TODO: figure out a way for the test to actually test this bit of code
- prompt.SelectedIndex = 2
+ prompt.selectedIndex = 2
- actual, err := RunTemplate(
+ actual, err := core.RunTemplate(
SelectChoicesTemplate,
- SelectTemplateData{Select: *prompt},
+ SelectTemplateData{Select: *prompt, SelectedIndex: 2},
)
if err != nil {
- t.Errorf("Failed to run template to format choice choices: %s", err)
+ t.Errorf("Failed to run template to format choice options: %s", err)
}
expected := ` foo
@@ -40,13 +42,13 @@ func TestCanFormatSelectOptions(t *testing.T) {
func TestSelectFormatQuestion(t *testing.T) {
- prompt := &Choice{
+ prompt := &Select{
Message: "Pick your word:",
- Choices: []string{"foo", "bar", "baz", "buz"},
+ Options: []string{"foo", "bar", "baz", "buz"},
Default: "baz",
}
- actual, err := RunTemplate(
+ actual, err := core.RunTemplate(
SelectQuestionTemplate,
SelectTemplateData{Select: *prompt},
)
@@ -63,13 +65,13 @@ func TestSelectFormatQuestion(t *testing.T) {
func TestSelectFormatAnswer(t *testing.T) {
- prompt := &Choice{
+ prompt := &Select{
Message: "Pick your word:",
- Choices: []string{"foo", "bar", "baz", "buz"},
+ Options: []string{"foo", "bar", "baz", "buz"},
Default: "baz",
}
- actual, err := RunTemplate(
+ actual, err := core.RunTemplate(
SelectQuestionTemplate,
SelectTemplateData{Select: *prompt, Answer: "buz"},
)
diff --git a/survey.go b/survey.go
index d1c20d90..6639176c 100644
--- a/survey.go
+++ b/survey.go
@@ -1,15 +1,16 @@
package survey
import (
+ "errors"
"fmt"
- "os"
- "github.com/alecaivazis/survey/terminal"
+ "github.com/AlecAivazis/survey/core"
+ "github.com/AlecAivazis/survey/terminal"
"github.com/chzyer/readline"
)
// Validator is a function passed to a Question in order to redefine
-type Validator func(string) error
+type Validator func(interface{}) error
// Question is the core data structure for a survey questionnaire.
type Question struct {
@@ -21,61 +22,53 @@ type Question struct {
// Prompt is the primary interface for the objects that can take user input
// and return a string value.
type Prompt interface {
- Prompt(*readline.Instance) (string, error)
- Cleanup(*readline.Instance, string) error
+ Prompt(*readline.Instance) (interface{}, error)
+ Cleanup(*readline.Instance, interface{}) error
}
var ErrorTemplate = `{{color "red"}}✘ Sorry, your reply was invalid: {{.Error}}{{color "reset"}}
`
// AskOne asks a single question without performing validation on the answer.
-func AskOne(p Prompt) (string, error) {
- answers, err := Ask([]*Question{{Name: "q1", Prompt: p}})
+func AskOne(p Prompt, t interface{}, v Validator) error {
+ err := Ask([]*Question{{Prompt: p, Validate: v}}, t)
if err != nil {
- return "", err
+ return err
}
- return answers["q1"], nil
-}
-
-// AskOneValidate asks a single question and validates the answer with v.
-func AskOneValidate(p Prompt, v Validator) (string, error) {
- answers, err := Ask([]*Question{{Name: "q1", Prompt: p, Validate: v}})
- return answers["q1"], err
-}
-func handleError(err error) {
- // tell the user what happened
- fmt.Println(err.Error())
- // quit the survey
- os.Exit(1)
+ return nil
}
// Ask performs the prompt loop
-func Ask(qs []*Question) (map[string]string, error) {
+func Ask(qs []*Question, t interface{}) error {
// grab the readline instance
rl, err := terminal.GetReadline()
if err != nil {
- handleError(err)
+ return err
+ }
+
+ // if we weren't passed a place to record the answers
+ if t == nil {
+ // we can't go any further
+ return errors.New("cannot call Ask() with a nil reference to record the answers")
}
- // the response map
- res := make(map[string]string)
// go over every question
for _, q := range qs {
// grab the user input and save it
ans, err := q.Prompt.Prompt(rl)
// if there was a problem
if err != nil {
- handleError(err)
+ return err
}
// if there is a validate handler for this question
if q.Validate != nil {
// wait for a valid response
for invalid := q.Validate(ans); invalid != nil; invalid = q.Validate(ans) {
- out, err := RunTemplate(ErrorTemplate, invalid)
+ out, err := core.RunTemplate(ErrorTemplate, invalid)
if err != nil {
- return nil, err
+ return err
}
// send the message to the user
fmt.Print(out)
@@ -83,7 +76,7 @@ func Ask(qs []*Question) (map[string]string, error) {
ans, err = q.Prompt.Prompt(rl)
// if there was a problem
if err != nil {
- handleError(err)
+ return err
}
}
}
@@ -94,11 +87,16 @@ func Ask(qs []*Question) (map[string]string, error) {
// if something went wrong
if err != nil {
// stop listening
- return nil, err
+ return err
}
+
// add it to the map
- res[q.Name] = ans
+ err = core.WriteAnswer(t, q.Name, ans)
+ // if something went wrong
+ if err != nil {
+ return err
+ }
}
// return the response
- return res, nil
+ return nil
}
diff --git a/survey_test.go b/survey_test.go
index dc8219d8..e59b51f4 100644
--- a/survey_test.go
+++ b/survey_test.go
@@ -3,18 +3,20 @@ package survey
import (
"fmt"
"testing"
+
+ "github.com/AlecAivazis/survey/core"
)
func init() {
// disable color output for all prompts to simplify testing
- DisableColor = true
+ core.DisableColor = true
}
func TestValidationError(t *testing.T) {
err := fmt.Errorf("Football is not a valid month")
- actual, err := RunTemplate(
+ actual, err := core.RunTemplate(
ErrorTemplate,
err,
)
@@ -29,3 +31,14 @@ func TestValidationError(t *testing.T) {
t.Errorf("Formatted error was not formatted correctly. Found:\n%s\nExpected:\n%s", actual, expected)
}
}
+
+func TestAsk_returnsErrorIfTargetIsNil(t *testing.T) {
+ // pass an empty place to leave the answers
+ err := Ask([]*Question{}, nil)
+
+ // if we didn't get an error
+ if err == nil {
+ // the test failed
+ t.Error("Did not encounter error when asking with no where to record.")
+ }
+}
diff --git a/tests/ask.go b/tests/ask.go
index af179cb2..dcd74d0e 100644
--- a/tests/ask.go
+++ b/tests/ask.go
@@ -2,7 +2,8 @@ package main
import (
"fmt"
- "github.com/alecaivazis/survey"
+
+ "github.com/AlecAivazis/survey"
)
// the questions to ask
@@ -16,9 +17,9 @@ var simpleQs = []*survey.Question{
},
{
Name: "color",
- Prompt: &survey.Choice{
+ Prompt: &survey.Select{
Message: "Choose a color:",
- Choices: []string{"red", "blue", "green", "yellow"},
+ Options: []string{"red", "blue", "green", "yellow"},
Default: "yellow",
},
Validate: survey.Required,
@@ -28,16 +29,20 @@ var simpleQs = []*survey.Question{
func main() {
fmt.Println("Asking many.")
-
- answers, err := survey.Ask(simpleQs)
+ // a place to store the answers
+ ans := struct {
+ Name string
+ Color string
+ }{}
+ err := survey.Ask(simpleQs, &ans)
if err != nil {
fmt.Println(err.Error())
return
}
- fmt.Printf("%s chose %s.\n", answers["name"], answers["color"])
fmt.Println("Asking one.")
- answer, err := survey.AskOne(simpleQs[0].Prompt)
+ answer := ""
+ err = survey.AskOne(simpleQs[0].Prompt, &answer, nil)
if err != nil {
fmt.Println(err.Error())
return
@@ -45,10 +50,11 @@ func main() {
fmt.Printf("Answered with %v.\n", answer)
fmt.Println("Asking one with validation.")
- answer, err = survey.AskOneValidate(&survey.Input{"What is your name?", ""}, survey.Required)
+ vAns := ""
+ err = survey.AskOne(&survey.Input{"What is your name?", ""}, &vAns, survey.Required)
if err != nil {
fmt.Println(err.Error())
return
}
- fmt.Printf("Answered with %v.\n", answer)
+ fmt.Printf("Answered with %v.\n", vAns)
}
diff --git a/tests/confirm.go b/tests/confirm.go
new file mode 100644
index 00000000..386b0a57
--- /dev/null
+++ b/tests/confirm.go
@@ -0,0 +1,37 @@
+package main
+
+import (
+ "github.com/AlecAivazis/survey"
+ "github.com/AlecAivazis/survey/tests/util"
+)
+
+var answer = false
+
+var goodTable = []TestUtil.TestTableEntry{
+ {
+ "Enter 'yes'", &survey.Confirm{
+ Message: "yes:",
+ }, &answer,
+ },
+ {
+ "Enter 'no'", &survey.Confirm{
+ Message: "yes:",
+ }, &answer,
+ },
+ {
+ "default", &survey.Confirm{
+ Message: "yes:",
+ Default: true,
+ }, &answer,
+ },
+ {
+ "not recognized (enter random letter)", &survey.Confirm{
+ Message: "yes:",
+ Default: true,
+ }, &answer,
+ },
+}
+
+func main() {
+ TestUtil.RunTable(goodTable)
+}
diff --git a/tests/input.go b/tests/input.go
index 9332ccc0..4c76189b 100644
--- a/tests/input.go
+++ b/tests/input.go
@@ -1,16 +1,18 @@
package main
import (
- "github.com/alecaivazis/survey"
- "github.com/alecaivazis/survey/tests/util"
+ "github.com/AlecAivazis/survey"
+ "github.com/AlecAivazis/survey/tests/util"
)
+var val = ""
+
var table = []TestUtil.TestTableEntry{
{
- "no default", &survey.Input{"Hello world", ""},
+ "no default", &survey.Input{"Hello world", ""}, &val,
},
{
- "default", &survey.Input{"Hello world", "default"},
+ "default", &survey.Input{"Hello world", "default"}, &val,
},
}
diff --git a/tests/multiselect.go b/tests/multiselect.go
new file mode 100644
index 00000000..8d2d6926
--- /dev/null
+++ b/tests/multiselect.go
@@ -0,0 +1,35 @@
+package main
+
+import (
+ "github.com/AlecAivazis/survey"
+ "github.com/AlecAivazis/survey/tests/util"
+)
+
+var answer = []string{}
+
+var table = []TestUtil.TestTableEntry{
+ {
+ "standard", &survey.MultiSelect{
+ Message: "What days do you prefer:",
+ Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
+ }, &answer,
+ },
+ {
+ "default (sunday, tuesday)", &survey.MultiSelect{
+ Message: "What days do you prefer:",
+ Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
+ Default: []string{"Sunday", "Tuesday"},
+ }, &answer,
+ },
+ {
+ "default not found", &survey.MultiSelect{
+ Message: "What days do you prefer:",
+ Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
+ Default: []string{"Sundayaa"},
+ }, &answer,
+ },
+}
+
+func main() {
+ TestUtil.RunTable(table)
+}
diff --git a/tests/password.go b/tests/password.go
index 92fba2b7..e37b293b 100644
--- a/tests/password.go
+++ b/tests/password.go
@@ -1,16 +1,18 @@
package main
import (
- "github.com/alecaivazis/survey"
- "github.com/alecaivazis/survey/tests/util"
+ "github.com/AlecAivazis/survey"
+ "github.com/AlecAivazis/survey/tests/util"
)
+var value = ""
+
var table = []TestUtil.TestTableEntry{
{
- "standard", &survey.Password{"Please type your password:"},
+ "standard", &survey.Password{"Please type your password:"}, &value,
},
{
- "please make sure paste works", &survey.Password{"Please paste your password:"},
+ "please make sure paste works", &survey.Password{"Please paste your password:"}, &value,
},
}
diff --git a/tests/select.go b/tests/select.go
index 6d62dd68..7a84a1eb 100644
--- a/tests/select.go
+++ b/tests/select.go
@@ -1,43 +1,45 @@
package main
import (
- "github.com/alecaivazis/survey"
- "github.com/alecaivazis/survey/tests/util"
+ "github.com/AlecAivazis/survey"
+ "github.com/AlecAivazis/survey/tests/util"
)
+var answer = ""
+
var goodTable = []TestUtil.TestTableEntry{
{
- "standard", &survey.Choice{
+ "standard", &survey.Select{
Message: "Choose a color:",
- Choices: []string{"red", "blue", "green"},
- },
+ Options: []string{"red", "blue", "green"},
+ }, &answer,
},
{
- "short", &survey.Choice{
+ "short", &survey.Select{
Message: "Choose a color:",
- Choices: []string{"red", "blue"},
- },
+ Options: []string{"red", "blue"},
+ }, &answer,
},
{
- "default", &survey.Choice{
+ "default", &survey.Select{
Message: "Choose a color (should default blue):",
- Choices: []string{"red", "blue", "green"},
+ Options: []string{"red", "blue", "green"},
Default: "blue",
- },
+ }, &answer,
},
{
- "one", &survey.Choice{
+ "one", &survey.Select{
Message: "Choose one:",
- Choices: []string{"hello"},
- },
+ Options: []string{"hello"},
+ }, &answer,
},
}
var badTable = []TestUtil.TestTableEntry{
{
- "no Choices", &survey.Choice{
+ "no options", &survey.Select{
Message: "Choose one:",
- },
+ }, &answer,
},
}
diff --git a/tests/util/test.go b/tests/util/test.go
index f70282a1..5b490206 100644
--- a/tests/util/test.go
+++ b/tests/util/test.go
@@ -2,18 +2,20 @@ package TestUtil
import (
"fmt"
+ "reflect"
- "github.com/alecaivazis/survey"
+ "github.com/AlecAivazis/survey"
)
type TestTableEntry struct {
Name string
Prompt survey.Prompt
+ Value interface{}
}
-func formatAnswer(ans string) {
+func formatAnswer(ans interface{}) {
// show the answer to the user
- fmt.Printf("Answered %v.\n", ans)
+ fmt.Printf("Answered %v.\n", reflect.ValueOf(ans).Elem())
fmt.Println("---------------------")
}
@@ -23,13 +25,13 @@ func RunTable(table []TestTableEntry) {
// tell the user what we are going to ask them
fmt.Println(entry.Name)
// perform the ask
- answer, err := survey.AskOne(entry.Prompt)
+ err := survey.AskOne(entry.Prompt, entry.Value, nil)
if err != nil {
fmt.Printf("AskOne on %v's prompt failed: %v.", entry.Name, err.Error())
break
}
// show the answer to the user
- formatAnswer(answer)
+ formatAnswer(entry.Value)
}
}
@@ -39,7 +41,7 @@ func RunErrorTable(table []TestTableEntry) {
// tell the user what we are going to ask them
fmt.Println(entry.Name)
// perform the ask
- _, err := survey.AskOne(entry.Prompt)
+ err := survey.AskOne(entry.Prompt, entry.Value, nil)
if err == nil {
fmt.Printf("AskOne on %v's prompt didn't fail.", entry.Name)
break
diff --git a/validate.go b/validate.go
index d4693409..6e275beb 100644
--- a/validate.go
+++ b/validate.go
@@ -3,27 +3,33 @@ package survey
import (
"errors"
"fmt"
+ "reflect"
)
// Required does not allow an empty value
-func Required(str string) error {
- // if the string is empty
- if str == "" {
- // return the error
- return errors.New("Value is required.")
+func Required(val interface{}) error {
+ // if the value passed in is the zero value of the appropriate type
+ if val == reflect.Zero(reflect.TypeOf(val)).Interface() {
+ return errors.New("Value is required")
}
- // nothing was wrong
return nil
}
// MaxLength requires that the string is no longer than the specified value
func MaxLength(length int) Validator {
// return a validator that checks the length of the string
- return func(str string) error {
- // if the string is longer than the given value
- if len(str) > length {
- return fmt.Errorf("Value is too long. Max length is %v.", length)
+ return func(val interface{}) error {
+ if str, ok := val.(string); ok {
+ // if the string is longer than the given value
+ if len(str) > length {
+ // yell loudly
+ return fmt.Errorf("value is too long. Max length is %v", length)
+ }
+ } else {
+ // otherwise we cannot convert the value into a string and cannot enforce length
+ return fmt.Errorf("cannot enforce length on response of type %v", reflect.TypeOf(val).Name())
}
+
// the input is fine
return nil
}
@@ -32,11 +38,18 @@ func MaxLength(length int) Validator {
// MinLength requires that the string is longer or equal in length to the specified value
func MinLength(length int) Validator {
// return a validator that checks the length of the string
- return func(str string) error {
- // if the string is longer than the given value
- if len(str) < length {
- return fmt.Errorf("Value is too short. Min length is %v.", length)
+ return func(val interface{}) error {
+ if str, ok := val.(string); ok {
+ // if the string is shorter than the given value
+ if len(str) < length {
+ // yell loudly
+ return fmt.Errorf("value is too short. Min length is %v", length)
+ }
+ } else {
+ // otherwise we cannot convert the value into a string and cannot enforce length
+ return fmt.Errorf("cannot enforce length on response of type %v", reflect.TypeOf(val).Name())
}
+
// the input is fine
return nil
}
@@ -45,11 +58,11 @@ func MinLength(length int) Validator {
// ComposeValidators is a variadic function used to create one validator from many.
func ComposeValidators(validators ...Validator) Validator {
// return a validator that calls each one sequentially
- return func(str string) error {
+ return func(val interface{}) error {
// execute each validator
for _, validator := range validators {
// if the string is not valid
- if err := validator(str); err != nil {
+ if err := validator(val); err != nil {
// return the error
return err
}
diff --git a/validate_test.go b/validate_test.go
index ed5f0369..601363c0 100644
--- a/validate_test.go
+++ b/validate_test.go
@@ -44,14 +44,26 @@ func TestMaxLength(t *testing.T) {
}
func TestMinLength(t *testing.T) {
- // the string to test
- testStr := randString(10)
// validate the string
- if err := MinLength(12)(testStr); err == nil {
+ if err := MinLength(12)(randString(10)); err == nil {
t.Error("No error returned with input less than 12 characters.")
}
}
+func TestMinLengthOnInt(t *testing.T) {
+ // validate the string
+ if err := MinLength(12)(1); err == nil {
+ t.Error("No error returned when enforcing length on int.")
+ }
+}
+
+func TestMaxLengthOnInt(t *testing.T) {
+ // validate the string
+ if err := MaxLength(12)(1); err == nil {
+ t.Error("No error returned when enforcing length on int.")
+ }
+}
+
func TestComposeValidatorsPasses(t *testing.T) {
// create a validator that requires a string of no more than 10 characters
valid := ComposeValidators(