A golang library for building interactive prompts with full support for windows and posix terminals.
Go
Clone or download
hinshun and AlecAivazis Refactor survey to use optionally specified stdio. (#143)
* Refactor survey to use optionally specified stdio.

* Update cursor up down with terminal key arrows

* Update contributing, readme with go-expect tests and remove autoplay
Latest commit db8e629 Jun 21, 2018
Permalink
Failed to load latest commit information.
core Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
examples Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
terminal Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
tests Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
vendor Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
.travis.yml [#89] replaced local imports with references to gopkg.in (#91) Aug 31, 2017
CONTRIBUTING.md Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
Gopkg.lock Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
Gopkg.toml Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
LICENSE Update LICENSE Mar 3, 2018
README.md Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
_tasks.yml renamed task file Sep 29, 2017
confirm.go Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
confirm_test.go Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
editor.go Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
editor_test.go Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
input.go Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
input_test.go Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
multiselect.go Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
multiselect_test.go Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
password.go Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
password_test.go Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
select.go Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
select_test.go Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
survey.go Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
survey_posix_test.go Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
survey_test.go Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
survey_windows_test.go Refactor survey to use optionally specified stdio. (#143) Jun 21, 2018
transform.go Implemented the `Transform` idea (#104) Oct 21, 2017
transform_test.go Implemented the `Transform` idea (#104) Oct 21, 2017
validate.go boolean values pass through the required validator (#123) Feb 26, 2018
validate_test.go boolean values pass through the required validator (#123) Feb 26, 2018

README.md

Survey

Build Status GoDoc

A library for building interactive prompts.

package main

import (
    "fmt"
    "gopkg.in/AlecAivazis/survey.v1"
)

// the questions to ask
var qs = []*survey.Question{
    {
        Name:     "name",
        Prompt:   &survey.Input{Message: "What is your name?"},
        Validate: survey.Required,
        Transform: survey.Title,
    },
    {
        Name: "color",
        Prompt: &survey.Select{
            Message: "Choose a color:",
            Options: []string{"red", "blue", "green"},
            Default: "red",
        },
    },
    {
        Name: "age",
        Prompt:   &survey.Input{Message: "How old are you?"},
    },
}

func main() {
    // 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
        Age           int                     // if the types don't match exactly, survey will try to convert for you
    }{}

    // perform the questions
    err := survey.Ask(qs, &answers)
    if err != nil {
        fmt.Println(err.Error())
        return
    }

    fmt.Printf("%s chose %s.", answers.Name, answers.FavoriteColor)
}

Table of Contents

  1. Examples
  2. Prompts
    1. Input
    2. Password
    3. Confirm
    4. Select
    5. MultiSelect
    6. Editor
  3. Validation
    1. Built-in Validators
  4. Help Text
    1. Changing the input rune
  5. Custom Types
  6. Customizing Output
  7. Versioning
  8. Testing

Examples

Examples can be found in the examples/ directory. Run them to see basic behavior:

go get gopkg.in/AlecAivazis/survey.v1

cd $GOPATH/src/gopkg.in/AlecAivazis/survey.v1

go run examples/simple.go
go run examples/validation.go

Prompts

Input

name := ""
prompt := &survey.Input{
    Message: "ping",
}
survey.AskOne(prompt, &name, nil)

Password

password := ""
prompt := &survey.Password{
    Message: "Please type your password",
}
survey.AskOne(prompt, &password, nil)

Confirm

name := false
prompt := &survey.Confirm{
    Message: "Do you like pie?",
}
survey.AskOne(prompt, &name, nil)

Select

color := ""
prompt := &survey.Select{
    Message: "Choose a color:",
    Options: []string{"red", "blue", "green"},
}
survey.AskOne(prompt, &color, nil)

The user can filter for options by typing while the prompt is active. The user can also press esc to toggle the ability cycle through the options with the j and k keys to do down and up respectively.

By default, the select prompt is limited to showing 7 options at a time and will paginate lists of options longer than that. To increase, you can either change the global survey.PageSize, or set the PageSize field on the prompt:

prompt := &survey.Select{..., PageSize: 10}

MultiSelect

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)

The user can filter for options by typing while the prompt is active. The user can also press esc to toggle the ability cycle through the options with the j and k keys to do down and up respectively.

By default, the MultiSelect prompt is limited to showing 7 options at a time and will paginate lists of options longer than that. To increase, you can either change the global survey.PageSize, or set the PageSize field on the prompt:

prompt := &survey.MultiSelect{..., PageSize: 10}

Editor

Launches the user's preferred editor (defined by the $EDITOR environment variable) on a temporary file. Once the user exits their editor, the contents of the temporary file are read in as the result. If neither of those are present, notepad (on Windows) or vim (Linux or Mac) is used.

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:

q := &survey.Question{
    Prompt: &survey.Input{Message: "Hello world validation"},
    Validate: func (val interface{}) error {
        // since we are validating an Input, the assertion will always succeed
        if str, ok := val.(string) ; !ok || 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 notes
Required any Rejects zero values of the response type Boolean values pass straight through since the zero value (false) is a valid response
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

Help Text

All of the prompts have a Help field which can be defined to provide more information to your users:

&survey.Input{
    Message: "What is your phone number:",
    Help:    "Phone number should include the area code",
}

Changing the input rune

In some situations, ? is a perfectly valid response. To handle this, you can change the rune that survey looks for by setting the HelpInputRune variable in survey/core:

import (
    "gopkg.in/AlecAivazis/survey.v1"
    surveyCore "gopkg.in/AlecAivazis/survey.v1/core"
)

number := ""
prompt := &survey.Input{
    Message: "If you have this need, please give me a reasonable message.",
    Help:    "I couldn't come up with one.",
}

surveyCore.HelpIcon = '^'

survey.AskOne(prompt, &number, nil)

Custom Types

survey will assign prompt answers to your custom types if they implement this interface:

type settable interface {
    WriteAnswer(field string, value interface{}) error
}

Here is an example how to use them:

type MyValue struct {
    value string
}
func (my *MyValue) WriteAnswer(name string, value interface{}) error {
     my.value = value.(string)
}

myval := MyValue{}
survey.AskOne(
    &survey.Input{
        Message: "Enter something:",
    },
    &myval,
    nil,
)

Customizing Output

Customizing the icons and various parts of survey can easily be done by setting the following variables in survey/core:

name default description
ErrorIcon Before an error
HelpIcon Before help text
QuestionIcon ? Before the message of a prompt
SelectFocusIcon Marks the current focus in Select and MultiSelect prompts
MarkedOptionIcon Marks a chosen selection in a MultiSelect prompt
UnmarkedOptionIcon Marks an unselected option in a MultiSelect prompt

Versioning

This project tries to maintain semantic GitHub releases as closely as possible and relies on gopkg.in to maintain those releases. Importing version 1 of survey would look like:

package main

import "gopkg.in/AlecAivazis/survey.v1"

Testing

You can test your program's interactive prompts using go-expect. The library can be used to expect a match on stdout and respond on stdin. Since os.Stdout in a go test process is not a TTY, if you are manipulating the cursor or using survey, you will need a way to interpret terminal / ANSI escape sequences for things like CursorLocation. vt10x.NewVT10XConsole will create a go-expect console that also multiplexes stdio to an in-memory virtual terminal.

For example, you can test a binary utilizing survey by connecting the Console's tty to a subprocess's stdio.

func TestCLI(t *testing.T) {
  // Multiplex stdin/stdout to a virtual terminal to respond to ANSI escape
  // sequences (i.e. cursor position report).
	c, state, err := vt10x.NewVT10XConsole()
	require.Nil(t, err)
  defer c.Close()

  donec := make(chan struct{})
  go func() {
    defer close(donec)
    c.ExpectString("What is your name?")
    c.SendLine("Johnny Appleseed")
    c.ExpectEOF()
  }()

  cmd := exec.Command("your-cli")
  cmd.Stdin = c.Tty()
  cmd.Stdout = c.Tty()
  cmd.Stderr = c.Tty()

  err = cmd.Run()
  require.Nil(t, err)

  // Close the slave end of the pty, and read the remaining bytes from the master end.
  c.Tty().Close()
  <-donec

  // Dump the terminal's screen.
  t.Log(expect.StripTrailingEmptyLines(state.String()))
}

If your application is decoupled from os.Stdout and os.Stdin, you can even test through the tty alone. survey itself is tested in this manner.

func TestCLI(t *testing.T) {
  // Multiplex stdin/stdout to a virtual terminal to respond to ANSI escape
  // sequences (i.e. cursor position report).
	c, state, err := vt10x.NewVT10XConsole()
	require.Nil(t, err)
  defer c.Close()

  donec := make(chan struct{})
  go func() {
    defer close(donec)
    c.ExpectString("What is your name?")
    c.SendLine("Johnny Appleseed")
    c.ExpectEOF()
  }()

  prompt := &Input{
    Message: "What is your name?",
  }
  prompt.WithStdio(Stdio(c))

  answer, err := prompt.Prompt()
  require.Nil(t, err)
  require.Equal(t, "Johnny Appleseed", answer)

  // Close the slave end of the pty, and read the remaining bytes from the master end.
  c.Tty().Close()
  <-donec

  // Dump the terminal's screen.
  t.Log(expect.StripTrailingEmptyLines(state.String()))
}