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