github.com/paultyng/terraform@v0.6.11-0.20180227224804-66ff8f8bed40/command/ui_input.go (about) 1 package command 2 3 import ( 4 "bufio" 5 "bytes" 6 "errors" 7 "fmt" 8 "io" 9 "log" 10 "os" 11 "os/signal" 12 "strings" 13 "sync" 14 "unicode" 15 16 "github.com/hashicorp/terraform/terraform" 17 "github.com/mitchellh/colorstring" 18 ) 19 20 var defaultInputReader io.Reader 21 var defaultInputWriter io.Writer 22 var testInputResponse []string 23 var testInputResponseMap map[string]string 24 25 // UIInput is an implementation of terraform.UIInput that asks the CLI 26 // for input stdin. 27 type UIInput struct { 28 // Colorize will color the output. 29 Colorize *colorstring.Colorize 30 31 // Reader and Writer for IO. If these aren't set, they will default to 32 // Stdout and Stderr respectively. 33 Reader io.Reader 34 Writer io.Writer 35 36 interrupted bool 37 l sync.Mutex 38 once sync.Once 39 } 40 41 func (i *UIInput) Input(opts *terraform.InputOpts) (string, error) { 42 i.once.Do(i.init) 43 44 r := i.Reader 45 w := i.Writer 46 if r == nil { 47 r = defaultInputReader 48 } 49 if w == nil { 50 w = defaultInputWriter 51 } 52 if r == nil { 53 r = os.Stdin 54 } 55 if w == nil { 56 w = os.Stdout 57 } 58 59 // Make sure we only ask for input once at a time. Terraform 60 // should enforce this, but it doesn't hurt to verify. 61 i.l.Lock() 62 defer i.l.Unlock() 63 64 // If we're interrupted, then don't ask for input 65 if i.interrupted { 66 return "", errors.New("interrupted") 67 } 68 69 // If we have test results, return those. testInputResponse is the 70 // "old" way of doing it and we should remove that. 71 if testInputResponse != nil { 72 v := testInputResponse[0] 73 testInputResponse = testInputResponse[1:] 74 return v, nil 75 } 76 77 // testInputResponseMap is the new way for test responses, based on 78 // the query ID. 79 if testInputResponseMap != nil { 80 v, ok := testInputResponseMap[opts.Id] 81 if !ok { 82 return "", fmt.Errorf("unexpected input request in test: %s", opts.Id) 83 } 84 85 return v, nil 86 } 87 88 log.Printf("[DEBUG] command: asking for input: %q", opts.Query) 89 90 // Listen for interrupts so we can cancel the input ask 91 sigCh := make(chan os.Signal, 1) 92 signal.Notify(sigCh, os.Interrupt) 93 defer signal.Stop(sigCh) 94 95 // Build the output format for asking 96 var buf bytes.Buffer 97 buf.WriteString("[reset]") 98 buf.WriteString(fmt.Sprintf("[bold]%s[reset]\n", opts.Query)) 99 if opts.Description != "" { 100 s := bufio.NewScanner(strings.NewReader(opts.Description)) 101 for s.Scan() { 102 buf.WriteString(fmt.Sprintf(" %s\n", s.Text())) 103 } 104 buf.WriteString("\n") 105 } 106 if opts.Default != "" { 107 buf.WriteString(" [bold]Default:[reset] ") 108 buf.WriteString(opts.Default) 109 buf.WriteString("\n") 110 } 111 buf.WriteString(" [bold]Enter a value:[reset] ") 112 113 // Ask the user for their input 114 if _, err := fmt.Fprint(w, i.Colorize.Color(buf.String())); err != nil { 115 return "", err 116 } 117 118 // Listen for the input in a goroutine. This will allow us to 119 // interrupt this if we are interrupted (SIGINT) 120 result := make(chan string, 1) 121 go func() { 122 buf := bufio.NewReader(r) 123 line, err := buf.ReadString('\n') 124 if err != nil { 125 log.Printf("[ERR] UIInput scan err: %s", err) 126 } 127 128 result <- strings.TrimRightFunc(line, unicode.IsSpace) 129 }() 130 131 select { 132 case line := <-result: 133 fmt.Fprint(w, "\n") 134 135 if line == "" { 136 line = opts.Default 137 } 138 139 return line, nil 140 case <-sigCh: 141 // Print a newline so that any further output starts properly 142 // on a new line. 143 fmt.Fprintln(w) 144 145 // Mark that we were interrupted so future Ask calls fail. 146 i.interrupted = true 147 148 return "", errors.New("interrupted") 149 } 150 } 151 152 func (i *UIInput) init() { 153 if i.Colorize == nil { 154 i.Colorize = &colorstring.Colorize{ 155 Colors: colorstring.DefaultColors, 156 Disable: true, 157 } 158 } 159 }