├── .travis.yml ├── LICENSE.md ├── README.md ├── actor.go ├── confirm.go ├── confirm_test.go ├── example_test.go ├── input.go ├── input_test.go └── interact_suite_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Deiwin Sarjas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interact 2 | A Golang utility belt for interacting with the user over a CLI 3 | 4 | [![Build Status](https://travis-ci.org/deiwin/interact.svg?branch=master)](https://travis-ci.org/deiwin/interact) 5 | [![Coverage](http://gocover.io/_badge/github.com/deiwin/interact?0)](http://gocover.io/github.com/deiwin/interact) 6 | [![GoDoc](https://godoc.org/github.com/deiwin/interact?status.svg)](https://godoc.org/github.com/deiwin/interact) 7 | 8 | ## Example interaction 9 | 10 | Code like this: 11 | ```go 12 | actor := interact.NewActor(os.Stdin, os.Stdout) 13 | 14 | message := "Please enter something that's not empty" 15 | notEmpty, err := actor.Prompt(message, checkNotEmpty) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | message = "Please enter a positive number" 20 | n1, err := actor.PromptAndRetry(message, checkNotEmpty, checkIsAPositiveNumber) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | message = "Please enter another positive number" 25 | n2, err := actor.PromptOptionalAndRetry(message, "7", checkNotEmpty, checkIsAPositiveNumber) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | fmt.Printf("Thanks! (%s, %s, %s)\n", notEmpty, n1, n2) 30 | ``` 31 | 32 | Can create an interaction like this: 33 | 34 | ![asciicast](https://cloud.githubusercontent.com/assets/2261897/7066876/6194ec42-decf-11e4-823a-019f921f52a1.gif) 35 | 36 | For a more comprehensive example see the [example test](https://github.com/deiwin/interact/blob/master/example_test.go). 37 | -------------------------------------------------------------------------------- /actor.go: -------------------------------------------------------------------------------- 1 | // Package interact is a utility belt for interacting with the user over a CLI 2 | package interact 3 | 4 | import ( 5 | "bufio" 6 | "io" 7 | ) 8 | 9 | // An Actor provides methods to interact with the user 10 | type Actor struct { 11 | rd *bufio.Reader 12 | w io.Writer 13 | } 14 | 15 | // NewActor creates a new Actor instance with the specified io.Reader 16 | func NewActor(rd io.Reader, w io.Writer) Actor { 17 | return Actor{bufio.NewReader(rd), w} 18 | } 19 | -------------------------------------------------------------------------------- /confirm.go: -------------------------------------------------------------------------------- 1 | package interact 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | // errNoOptionSelected is returned when the user has not selected either yes or no 11 | errNoOptionSelected = errors.New("Please select y/n!") 12 | ) 13 | 14 | // ConfirmDefault specifies what an empty user input defaults to 15 | type ConfirmDefault int 16 | 17 | // Possible options for what an empty user input defaults to 18 | const ( 19 | ConfirmDefaultToYes ConfirmDefault = iota 20 | ConfirmDefaultToNo 21 | ConfirmNoDefault 22 | ) 23 | 24 | // Confirm provides the message to the user and asks yes or no. If the user 25 | // doesn't select either of the possible answers they will be prompted to answer 26 | // again until they do 27 | func (a Actor) Confirm(message string, def ConfirmDefault) (confirmed bool, err error) { 28 | for { 29 | confirmed, err := a.confirmOnce(message, def) 30 | if err == errNoOptionSelected { 31 | fmt.Fprintln(a.w, err) 32 | continue 33 | } 34 | return confirmed, err 35 | } 36 | } 37 | 38 | func (a Actor) confirmOnce(message string, def ConfirmDefault) (bool, error) { 39 | var options string 40 | switch def { 41 | case ConfirmDefaultToYes: 42 | options = "[Y/n]" 43 | case ConfirmDefaultToNo: 44 | options = "[y/N]" 45 | case ConfirmNoDefault: 46 | options = "[y/n]" 47 | } 48 | fmt.Fprintf(a.w, "%s %s: ", message, options) 49 | 50 | line, err := a.rd.ReadString('\n') 51 | input := strings.TrimSpace(line) 52 | if err != nil { 53 | return false, err 54 | } else if input == "" { 55 | switch def { 56 | case ConfirmDefaultToYes: 57 | return true, nil 58 | case ConfirmDefaultToNo: 59 | return false, nil 60 | case ConfirmNoDefault: 61 | return false, errNoOptionSelected 62 | } 63 | } 64 | switch input { 65 | case "y": 66 | return true, nil 67 | case "n": 68 | return false, nil 69 | } 70 | return false, errNoOptionSelected 71 | } 72 | -------------------------------------------------------------------------------- /confirm_test.go: -------------------------------------------------------------------------------- 1 | package interact_test 2 | 3 | import ( 4 | "github.com/deiwin/interact" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | "github.com/onsi/gomega/gbytes" 9 | ) 10 | 11 | var _ = Describe("Confirm", func() { 12 | var ( 13 | def interact.ConfirmDefault 14 | message = "Are you sure?" 15 | ) 16 | 17 | Context("with no default", func() { 18 | BeforeEach(func() { 19 | def = interact.ConfirmNoDefault 20 | }) 21 | 22 | It("should ask with yes displayed as default", func() { 23 | actor.Confirm(message, def) 24 | Eventually(output).Should(gbytes.Say(`Are you sure\? \[y/n\]: `)) 25 | }) 26 | 27 | Context("with user answering yes", func() { 28 | BeforeEach(func() { 29 | userInput = "y\n" 30 | }) 31 | 32 | It("should return true", func() { 33 | confirmed, err := actor.Confirm(message, def) 34 | Expect(err).NotTo(HaveOccurred()) 35 | Expect(confirmed).To(BeTrue()) 36 | }) 37 | }) 38 | 39 | Context("with user answering no", func() { 40 | BeforeEach(func() { 41 | userInput = "n\n" 42 | }) 43 | 44 | It("should return false", func() { 45 | confirmed, err := actor.Confirm(message, def) 46 | Expect(err).NotTo(HaveOccurred()) 47 | Expect(confirmed).To(BeFalse()) 48 | }) 49 | }) 50 | 51 | Context("with user answering nothing and then y", func() { 52 | BeforeEach(func() { 53 | userInput = "\ny\n" 54 | }) 55 | 56 | It("should return true", func() { 57 | confirmed, err := actor.Confirm(message, def) 58 | Expect(err).NotTo(HaveOccurred()) 59 | Expect(confirmed).To(BeTrue()) 60 | }) 61 | }) 62 | 63 | Context("with user answering gibberish and then y", func() { 64 | BeforeEach(func() { 65 | userInput = "asdfsadfa\ny\n" 66 | }) 67 | 68 | It("should return true", func() { 69 | confirmed, err := actor.Confirm(message, def) 70 | Expect(err).NotTo(HaveOccurred()) 71 | Expect(confirmed).To(BeTrue()) 72 | }) 73 | 74 | It("should ask twice with yes displayed as default", func() { 75 | actor.Confirm(message, def) 76 | Eventually(output).Should(gbytes.Say(`Are you sure\? \[y/n\]: `)) 77 | Eventually(output).Should(gbytes.Say(`Please select y/n`)) 78 | Eventually(output).Should(gbytes.Say(`Are you sure\? \[y/n\]: `)) 79 | }) 80 | }) 81 | }) 82 | 83 | Context("with no as default", func() { 84 | BeforeEach(func() { 85 | def = interact.ConfirmDefaultToNo 86 | }) 87 | 88 | It("should ask with yes displayed as default", func() { 89 | actor.Confirm(message, def) 90 | Eventually(output).Should(gbytes.Say(`Are you sure\? \[y/N\]: `)) 91 | }) 92 | 93 | Context("with user answering yes", func() { 94 | BeforeEach(func() { 95 | userInput = "y\n" 96 | }) 97 | 98 | It("should return true", func() { 99 | confirmed, err := actor.Confirm(message, def) 100 | Expect(err).NotTo(HaveOccurred()) 101 | Expect(confirmed).To(BeTrue()) 102 | }) 103 | }) 104 | 105 | Context("with user answering no", func() { 106 | BeforeEach(func() { 107 | userInput = "n\n" 108 | }) 109 | 110 | It("should return false", func() { 111 | confirmed, err := actor.Confirm(message, def) 112 | Expect(err).NotTo(HaveOccurred()) 113 | Expect(confirmed).To(BeFalse()) 114 | }) 115 | }) 116 | 117 | Context("with user answering nothing", func() { 118 | BeforeEach(func() { 119 | userInput = "\n" 120 | }) 121 | 122 | It("should return false", func() { 123 | confirmed, err := actor.Confirm(message, def) 124 | Expect(err).NotTo(HaveOccurred()) 125 | Expect(confirmed).To(BeFalse()) 126 | }) 127 | }) 128 | 129 | Context("with user answering gibberish and then y", func() { 130 | BeforeEach(func() { 131 | userInput = "asdfasdf\ny\n" 132 | }) 133 | 134 | It("should return true", func() { 135 | confirmed, err := actor.Confirm(message, def) 136 | Expect(err).NotTo(HaveOccurred()) 137 | Expect(confirmed).To(BeTrue()) 138 | }) 139 | }) 140 | }) 141 | 142 | Context("with yes as default", func() { 143 | BeforeEach(func() { 144 | def = interact.ConfirmDefaultToYes 145 | }) 146 | 147 | It("should ask with yes displayed as default", func() { 148 | actor.Confirm(message, def) 149 | Eventually(output).Should(gbytes.Say(`Are you sure\? \[Y/n\]: `)) 150 | }) 151 | 152 | Context("with user answering yes", func() { 153 | BeforeEach(func() { 154 | userInput = "y\n" 155 | }) 156 | 157 | It("should return true", func() { 158 | confirmed, err := actor.Confirm(message, def) 159 | Expect(err).NotTo(HaveOccurred()) 160 | Expect(confirmed).To(BeTrue()) 161 | }) 162 | }) 163 | 164 | Context("with user answering no", func() { 165 | BeforeEach(func() { 166 | userInput = "n\n" 167 | }) 168 | 169 | It("should return false", func() { 170 | confirmed, err := actor.Confirm(message, def) 171 | Expect(err).NotTo(HaveOccurred()) 172 | Expect(confirmed).To(BeFalse()) 173 | }) 174 | }) 175 | 176 | Context("with user answering nothing", func() { 177 | BeforeEach(func() { 178 | userInput = "\n" 179 | }) 180 | 181 | It("should return true", func() { 182 | confirmed, err := actor.Confirm(message, def) 183 | Expect(err).NotTo(HaveOccurred()) 184 | Expect(confirmed).To(BeTrue()) 185 | }) 186 | }) 187 | 188 | Context("with user answering gibberish and then y", func() { 189 | BeforeEach(func() { 190 | userInput = "sadfasdf\ny\n" 191 | }) 192 | 193 | It("should return true", func() { 194 | confirmed, err := actor.Confirm(message, def) 195 | Expect(err).NotTo(HaveOccurred()) 196 | Expect(confirmed).To(BeTrue()) 197 | }) 198 | }) 199 | }) 200 | }) 201 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package interact_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strconv" 10 | 11 | "github.com/deiwin/interact" 12 | ) 13 | 14 | // First let's declare some simple input validators 15 | var ( 16 | checkNotEmpty = func(input string) error { 17 | // note that the inputs provided to these checks are already trimmed 18 | if input == "" { 19 | return errors.New("Input should not be empty!") 20 | } 21 | return nil 22 | } 23 | checkIsAPositiveNumber = func(input string) error { 24 | if n, err := strconv.Atoi(input); err != nil { 25 | return err 26 | } else if n < 0 { 27 | return errors.New("The number can not be negative!") 28 | } 29 | return nil 30 | } 31 | ) 32 | 33 | func Example() { 34 | var userInput bytes.Buffer 35 | var b = NewTestBuffer(&userInput, os.Stdout) 36 | // Normally you would initiate the actor with os.Stdin and os.Stdout, but to 37 | // make this example test work nice we need to use something different 38 | actor := interact.NewActor(b, b) 39 | 40 | // A simple prompt for non-empty input 41 | userInput.WriteString("hello\n") // To keep the test simple we have to provide user inputs up front 42 | if result, err := actor.Prompt("Please enter something that's not empty", checkNotEmpty); err != nil { 43 | log.Fatal(err) 44 | } else if result != "hello" { 45 | log.Fatalf("Expected 'hello', got '%s'", result) 46 | } 47 | 48 | // A more complex example with the user retrying 49 | userInput.WriteString("-2\ny\n5\n") 50 | if result, err := actor.PromptAndRetry("Please enter a positive number", checkNotEmpty, checkIsAPositiveNumber); err != nil { 51 | log.Fatal(err) 52 | } else if result != "5" { 53 | log.Fatalf("Expected '5', got '%s'", result) 54 | } 55 | 56 | // An example with the user retrying and then opting to use the default value 57 | userInput.WriteString("-2\ny\n\n") 58 | if result, err := actor.PromptOptionalAndRetry("Please enter another positive number", "7", checkNotEmpty, checkIsAPositiveNumber); err != nil { 59 | log.Fatal(err) 60 | } else if result != "7" { 61 | log.Fatalf("Expected '7', got '%s'", result) 62 | } 63 | 64 | // This will force the last user input to be printed as well 65 | fmt.Fprint(b, "") 66 | 67 | // Output: 68 | // Please enter something that's not empty: hello 69 | // Please enter a positive number: -2 70 | // The number can not be negative! 71 | // Do you want to try again? [y/N]: y 72 | // Please enter a positive number: 5 73 | // Please enter another positive number: (7) -2 74 | // The number can not be negative! 75 | // Do you want to try again? [y/N]: y 76 | // Please enter another positive number: (7) 77 | } 78 | -------------------------------------------------------------------------------- /input.go: -------------------------------------------------------------------------------- 1 | package interact 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | errCanceled = errors.New("Command aborted") 11 | ) 12 | 13 | // InputCheck specifies the function signature for an input check 14 | type InputCheck func(string) error 15 | 16 | // PromptAndRetry asks the user for input and performs the list of added checks 17 | // on the provided input. If any of the checks fail to pass the error will be 18 | // displayed to the user and they will then be asked if they want to try again. 19 | // If the user does not want to retry the program will return an error. 20 | func (a Actor) PromptAndRetry(message string, checks ...InputCheck) (string, error) { 21 | for { 22 | input, err := a.Prompt(message, checks...) 23 | if err != nil { 24 | if err = a.confirmRetry(err); err != nil { 25 | return "", err 26 | } 27 | continue 28 | } 29 | return input, nil 30 | } 31 | } 32 | 33 | // PromptOptionalAndRetry works exactly like GetInputAndRetry, but also has 34 | // a default option which will be used instead if the user simply presses enter. 35 | func (a Actor) PromptOptionalAndRetry(message, defaultOption string, checks ...InputCheck) (string, error) { 36 | for { 37 | input, err := a.PromptOptional(message, defaultOption, checks...) 38 | if err != nil { 39 | if err = a.confirmRetry(err); err != nil { 40 | return "", err 41 | } 42 | continue 43 | } 44 | return input, nil 45 | } 46 | } 47 | 48 | // Prompt asks the user for input and performs the list of added checks on the 49 | // provided input. If any of the checks fail, the error will be returned. 50 | func (a Actor) Prompt(message string, checks ...InputCheck) (string, error) { 51 | input, err := a.prompt(message + ": ") 52 | if err != nil { 53 | return "", err 54 | } 55 | err = runChecks(input, checks...) 56 | if err != nil { 57 | return "", err 58 | } 59 | return input, nil 60 | } 61 | 62 | // PromptOptional works exactly like Prompt, but also has a default option 63 | // which will be used instead if the user simply presses enter. 64 | func (a Actor) PromptOptional(message, defaultOption string, checks ...InputCheck) (string, error) { 65 | input, err := a.prompt(fmt.Sprintf("%s: (%s) ", message, defaultOption)) 66 | if err != nil { 67 | return "", err 68 | } 69 | if input == "" { 70 | return defaultOption, nil 71 | } 72 | err = runChecks(input, checks...) 73 | if err != nil { 74 | return "", err 75 | } 76 | return input, nil 77 | } 78 | 79 | func (a Actor) confirmRetry(err error) error { 80 | retryMessage := fmt.Sprintf("%v\nDo you want to try again?", err) 81 | confirmed, err := a.Confirm(retryMessage, ConfirmDefaultToNo) 82 | if err != nil { 83 | return err 84 | } else if !confirmed { 85 | return errCanceled 86 | } 87 | return nil 88 | } 89 | 90 | func (a Actor) prompt(message string) (string, error) { 91 | fmt.Fprint(a.w, message) 92 | line, err := a.rd.ReadString('\n') 93 | if err != nil { 94 | return "", err 95 | } 96 | return strings.TrimSpace(line), nil 97 | } 98 | 99 | func runChecks(input string, checks ...InputCheck) error { 100 | for _, check := range checks { 101 | err := check(input) 102 | if err != nil { 103 | return err 104 | } 105 | } 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /input_test.go: -------------------------------------------------------------------------------- 1 | package interact_test 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/deiwin/interact" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | "github.com/onsi/gomega/gbytes" 10 | ) 11 | 12 | var _ = Describe("Input", func() { 13 | var message = "Please answer" 14 | Describe("PromptAndRetry", func() { 15 | Context("without any checks", func() { 16 | BeforeEach(func() { 17 | userInput = " user-input \n" 18 | }) 19 | 20 | It("should return the trimmed input (just as without retry)", func() { 21 | input, err := actor.PromptAndRetry(message) 22 | Expect(err).NotTo(HaveOccurred()) 23 | Expect(input).To(Equal("user-input")) 24 | Eventually(output).Should(gbytes.Say(`Please answer: `)) 25 | }) 26 | }) 27 | 28 | Context("with a check that fails the first time", func() { 29 | var ( 30 | checkErr = errors.New("The first time fails!") 31 | check interact.InputCheck 32 | i int 33 | ) 34 | BeforeEach(func() { 35 | i = 0 36 | check = func(input string) error { 37 | if i == 0 { 38 | Expect(input).To(Equal("user-input1")) 39 | i++ 40 | return checkErr 41 | } 42 | Expect(input).To(Equal("correct-input")) 43 | return nil 44 | } 45 | }) 46 | 47 | Context("with user retrying", func() { 48 | BeforeEach(func() { 49 | userInput = "user-input1\ny\ncorrect-input\n" 50 | }) 51 | 52 | It("should return the second (correct) input", func() { 53 | input, err := actor.PromptAndRetry(message, check) 54 | Expect(err).NotTo(HaveOccurred()) 55 | Expect(input).To(Equal("correct-input")) 56 | }) 57 | 58 | It("should have correct prompts", func() { 59 | actor.PromptAndRetry(message, check) 60 | Eventually(output).Should(gbytes.Say(`Please answer: `)) 61 | Eventually(output).Should(gbytes.Say(`The first time fails!`)) 62 | Eventually(output).Should(gbytes.Say(`Do you want to try again\? \[y/N\]: `)) 63 | Eventually(output).Should(gbytes.Say(`Please answer: `)) 64 | }) 65 | }) 66 | 67 | Context("with user retrying, but answering gibberish on first retry", func() { 68 | BeforeEach(func() { 69 | userInput = "user-input1\nasdfsadf\ny\ncorrect-input\n" 70 | }) 71 | 72 | It("should return the second (correct) input", func() { 73 | input, err := actor.PromptAndRetry(message, check) 74 | Expect(err).NotTo(HaveOccurred()) 75 | Expect(input).To(Equal("correct-input")) 76 | }) 77 | 78 | It("should have correct prompts", func() { 79 | actor.PromptAndRetry(message, check) 80 | Eventually(output).Should(gbytes.Say(`Please answer: `)) 81 | Eventually(output).Should(gbytes.Say(`The first time fails!`)) 82 | Eventually(output).Should(gbytes.Say(`Do you want to try again\? \[y/N\]: `)) 83 | Eventually(output).Should(gbytes.Say(`Please select y/n!`)) 84 | Eventually(output).Should(gbytes.Say(`Do you want to try again\? \[y/N\]: `)) 85 | Eventually(output).Should(gbytes.Say(`Please answer: `)) 86 | }) 87 | }) 88 | }) 89 | }) 90 | 91 | Describe("Prompt", func() { 92 | Context("with user input", func() { 93 | BeforeEach(func() { 94 | userInput = " user-input \n" 95 | }) 96 | 97 | It("should return the trimmed input", func() { 98 | input, err := actor.Prompt(message) 99 | Expect(err).NotTo(HaveOccurred()) 100 | Expect(input).To(Equal("user-input")) 101 | Eventually(output).Should(gbytes.Say(`Please answer: `)) 102 | }) 103 | 104 | Context("with a check", func() { 105 | var ( 106 | checkErr error 107 | check interact.InputCheck 108 | ) 109 | 110 | JustBeforeEach(func() { 111 | check = func(input string) error { 112 | Expect(input).To(Equal("user-input")) 113 | return checkErr 114 | } 115 | }) 116 | Context("with a failing check", func() { 117 | BeforeEach(func() { 118 | checkErr = errors.New("Check failed!") 119 | }) 120 | 121 | It("should return the error from the check", func() { 122 | _, err := actor.Prompt(message, check) 123 | Expect(err).To(Equal(checkErr)) 124 | }) 125 | 126 | Context("with another check after the failed one", func() { 127 | It("should not call the second check", func() { 128 | actor.Prompt(message, check, func(input string) error { 129 | Fail("should not be called") 130 | return nil 131 | }) 132 | }) 133 | }) 134 | }) 135 | 136 | Context("with a passing check", func() { 137 | BeforeEach(func() { 138 | checkErr = nil 139 | }) 140 | 141 | It("should not return an error", func() { 142 | _, err := actor.Prompt(message, check) 143 | Expect(err).NotTo(HaveOccurred()) 144 | }) 145 | }) 146 | }) 147 | }) 148 | }) 149 | 150 | Describe("PromptOptional", func() { 151 | var def = "default value" 152 | Context("without any checks", func() { 153 | Context("with a simple input", func() { 154 | BeforeEach(func() { 155 | userInput = " user-input \n" 156 | }) 157 | 158 | It("should return the trimmed input", func() { 159 | input, err := actor.PromptOptional(message, def) 160 | Expect(err).NotTo(HaveOccurred()) 161 | Expect(input).To(Equal("user-input")) 162 | Eventually(output).Should(gbytes.Say(`Please answer: `)) 163 | }) 164 | }) 165 | 166 | Context("with no input (just enter)", func() { 167 | BeforeEach(func() { 168 | userInput = "\n" 169 | }) 170 | 171 | It("should return the default value", func() { 172 | input, err := actor.PromptOptional(message, def) 173 | Expect(err).NotTo(HaveOccurred()) 174 | Expect(input).To(Equal(def)) 175 | Eventually(output).Should(gbytes.Say(`Please answer: \(default value\) `)) 176 | }) 177 | }) 178 | 179 | Context("with just whitespace as input", func() { 180 | BeforeEach(func() { 181 | userInput = " \n" 182 | }) 183 | 184 | It("should return the default value", func() { 185 | input, err := actor.PromptOptional(message, def) 186 | Expect(err).NotTo(HaveOccurred()) 187 | Expect(input).To(Equal(def)) 188 | Eventually(output).Should(gbytes.Say(`Please answer: \(default value\) `)) 189 | }) 190 | }) 191 | }) 192 | 193 | Context("with a check", func() { 194 | var ( 195 | checkErr error 196 | check interact.InputCheck 197 | ) 198 | JustBeforeEach(func() { 199 | check = func(input string) error { 200 | Expect(input).To(Equal("user-input")) 201 | return checkErr 202 | } 203 | }) 204 | 205 | Context("with no input (just enter), but a failing check", func() { 206 | BeforeEach(func() { 207 | userInput = "\n" 208 | checkErr = errors.New("Check failed!") 209 | }) 210 | 211 | It("should return the default value (not run the check)", func() { 212 | input, err := actor.PromptOptional(message, def, check) 213 | Expect(err).NotTo(HaveOccurred()) 214 | Expect(input).To(Equal(def)) 215 | Eventually(output).Should(gbytes.Say(`Please answer: \(default value\) `)) 216 | }) 217 | }) 218 | 219 | Context("with a simple input", func() { 220 | 221 | BeforeEach(func() { 222 | userInput = "user-input \n" 223 | }) 224 | 225 | Context("with a failing check", func() { 226 | BeforeEach(func() { 227 | checkErr = errors.New("Check failed!") 228 | }) 229 | 230 | It("should return the error from the check", func() { 231 | _, err := actor.PromptOptional(message, def, check) 232 | Expect(err).To(Equal(checkErr)) 233 | }) 234 | 235 | Context("with another check after the failed one", func() { 236 | It("should not call the second check", func() { 237 | actor.PromptOptional(message, def, check, func(input string) error { 238 | Fail("should not be called") 239 | return nil 240 | }) 241 | }) 242 | }) 243 | }) 244 | 245 | Context("with a passing check", func() { 246 | BeforeEach(func() { 247 | checkErr = nil 248 | }) 249 | 250 | It("should not return an error", func() { 251 | _, err := actor.PromptOptional(message, def, check) 252 | Expect(err).NotTo(HaveOccurred()) 253 | }) 254 | }) 255 | }) 256 | }) 257 | }) 258 | 259 | Describe("PromptOptionalAndRetry", func() { 260 | var def = "default value" 261 | Context("without any checks", func() { 262 | BeforeEach(func() { 263 | userInput = " user-input \n" 264 | }) 265 | 266 | It("should return the trimmed input (just as without retry)", func() { 267 | input, err := actor.PromptOptionalAndRetry(message, def) 268 | Expect(err).NotTo(HaveOccurred()) 269 | Expect(input).To(Equal("user-input")) 270 | Eventually(output).Should(gbytes.Say(`Please answer: `)) 271 | }) 272 | }) 273 | 274 | Context("with a check that fails the first time", func() { 275 | var ( 276 | checkErr = errors.New("The first time fails!") 277 | check interact.InputCheck 278 | i int 279 | ) 280 | BeforeEach(func() { 281 | i = 0 282 | check = func(input string) error { 283 | if i == 0 { 284 | Expect(input).To(Equal("user-input1")) 285 | i++ 286 | return checkErr 287 | } 288 | Expect(input).To(Equal("correct-input")) 289 | return nil 290 | } 291 | }) 292 | 293 | Context("with user retrying", func() { 294 | BeforeEach(func() { 295 | userInput = "user-input1\ny\ncorrect-input\n" 296 | }) 297 | 298 | It("should return the second (correct) input", func() { 299 | input, err := actor.PromptOptionalAndRetry(message, def, check) 300 | Expect(err).NotTo(HaveOccurred()) 301 | Expect(input).To(Equal("correct-input")) 302 | }) 303 | 304 | It("should have correct prompts", func() { 305 | actor.PromptOptionalAndRetry(message, def, check) 306 | Eventually(output).Should(gbytes.Say(`Please answer: \(default value\) `)) 307 | Eventually(output).Should(gbytes.Say(`The first time fails!`)) 308 | Eventually(output).Should(gbytes.Say(`Do you want to try again\? \[y/N\]: `)) 309 | Eventually(output).Should(gbytes.Say(`Please answer: \(default value\) `)) 310 | }) 311 | }) 312 | }) 313 | }) 314 | }) 315 | -------------------------------------------------------------------------------- /interact_suite_test.go: -------------------------------------------------------------------------------- 1 | package interact_test 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | "github.com/deiwin/interact" 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gbytes" 11 | 12 | "testing" 13 | ) 14 | 15 | func TestInteract(t *testing.T) { 16 | RegisterFailHandler(Fail) 17 | RunSpecs(t, "Interact Suite") 18 | } 19 | 20 | var ( 21 | actor interact.Actor 22 | userInput = "\n" 23 | output *gbytes.Buffer 24 | ) 25 | 26 | var _ = BeforeEach(func() { 27 | output = gbytes.NewBuffer() 28 | }) 29 | 30 | var _ = JustBeforeEach(func() { 31 | actor = interact.NewActor(strings.NewReader(userInput), output) 32 | }) 33 | 34 | func NewTestBuffer(r io.Reader, w io.Writer) *TestBuffer { 35 | return &TestBuffer{ 36 | r: r, 37 | w: w, 38 | userInput: make(chan string, 10), 39 | } 40 | } 41 | 42 | // TestBuffer is a hack that makes both the test and the test log easy to read 43 | type TestBuffer struct { 44 | r io.Reader 45 | w io.Writer 46 | userInput chan string 47 | } 48 | 49 | func (b *TestBuffer) Read(p []byte) (int, error) { 50 | n, err := b.r.Read(p) 51 | if err != nil { 52 | return 0, err 53 | } 54 | s := strings.TrimSuffix(string(p[:n]), "\n") 55 | for _, line := range strings.Split(s, "\n") { 56 | b.userInput <- line 57 | } 58 | return n, err 59 | } 60 | 61 | func (b *TestBuffer) Write(p []byte) (int, error) { 62 | var prefix string 63 | select { 64 | case i := <-b.userInput: 65 | prefix = i + "\n" 66 | default: 67 | } 68 | return b.w.Write(append([]byte(prefix), p...)) 69 | } 70 | --------------------------------------------------------------------------------