github.com/cmalfait/terraform@v0.11.12-beta1/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  }