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