package command import ( "bufio" "errors" "fmt" "io" "strings" "github.com/chzyer/readline" "github.com/hashicorp/packer/helper/wrappedreadline" "github.com/hashicorp/packer/helper/wrappedstreams" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/template" "github.com/hashicorp/packer/template/interpolate" "github.com/posener/complete" ) const TiniestBuilder = `{ "builders": [ { "type":"null", "communicator": "none" } ] }` type ConsoleCommand struct { Meta } func (c *ConsoleCommand) Run(args []string) int { flags := c.Meta.FlagSet("console", FlagSetVars) flags.Usage = func() { c.Ui.Say(c.Help()) } if err := flags.Parse(args); err != nil { return 1 } var templ *template.Template args = flags.Args() if len(args) < 1 { // If user has not defined a builder, create a tiny null placeholder // builder so that we can properly initialize the core tpl, err := template.Parse(strings.NewReader(TiniestBuilder)) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to generate placeholder template: %s", err)) return 1 } templ = tpl } else if len(args) == 1 { // Parse the provided template tpl, err := template.ParseFile(args[0]) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to parse template: %s", err)) return 1 } templ = tpl } else { // User provided too many arguments flags.Usage() return 1 } // Get the core core, err := c.Meta.Core(templ) if err != nil { c.Ui.Error(err.Error()) return 1 } // IO Loop session := &REPLSession{ Core: core, } // Determine if stdin is a pipe. If so, we evaluate directly. if c.StdinPiped() { return c.modePiped(session) } return c.modeInteractive(session) } func (*ConsoleCommand) Help() string { helpText := ` Usage: packer console [options] [TEMPLATE] Creates a console for testing variable interpolation. If a template is provided, this command will load the template and any variables defined therein into its context to be referenced during interpolation. Options: -var 'key=value' Variable for templates, can be used multiple times. -var-file=path JSON file containing user variables. [ Note that even in HCL mode this expects file to contain JSON, a fix is comming soon ] ` return strings.TrimSpace(helpText) } func (*ConsoleCommand) Synopsis() string { return "creates a console for testing variable interpolation" } func (*ConsoleCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing } func (*ConsoleCommand) AutocompleteFlags() complete.Flags { return complete.Flags{ "-var": complete.PredictNothing, "-var-file": complete.PredictNothing, } } func (c *ConsoleCommand) modePiped(session *REPLSession) int { var lastResult string scanner := bufio.NewScanner(wrappedstreams.Stdin()) for scanner.Scan() { result, err := session.Handle(strings.TrimSpace(scanner.Text())) if err != nil { return 0 } // Store the last result lastResult = result } // Output the final result c.Ui.Message(lastResult) return 0 } func (c *ConsoleCommand) modeInteractive(session *REPLSession) int { // Setup the UI so we can output directly to stdout l, err := readline.NewEx(wrappedreadline.Override(&readline.Config{ Prompt: "> ", InterruptPrompt: "^C", EOFPrompt: "exit", HistorySearchFold: true, })) if err != nil { c.Ui.Error(fmt.Sprintf( "Error initializing console: %s", err)) return 1 } for { // Read a line line, err := l.Readline() if err == readline.ErrInterrupt { if len(line) == 0 { break } else { continue } } else if err == io.EOF { break } out, err := session.Handle(line) if err == ErrSessionExit { break } if err != nil { c.Ui.Error(err.Error()) continue } c.Ui.Say(out) } return 0 } // ErrSessionExit is a special error result that should be checked for // from Handle to signal a graceful exit. var ErrSessionExit = errors.New("Session exit") // Session represents the state for a single Read-Evaluate-Print-Loop (REPL) session. type REPLSession struct { // Core is used for constructing interpolations based off packer templates Core *packer.Core } // Handle a single line of input from the REPL. // // The return value is the output and the error to show. func (s *REPLSession) Handle(line string) (string, error) { switch { case strings.TrimSpace(line) == "": return "", nil case strings.TrimSpace(line) == "exit": return "", ErrSessionExit case strings.TrimSpace(line) == "help": return s.handleHelp() case strings.TrimSpace(line) == "variables": return s.handleVariables() default: return s.handleEval(line) } } func (s *REPLSession) handleEval(line string) (string, error) { ctx := s.Core.Context() rendered, err := interpolate.Render(line, ctx) if err != nil { return "", fmt.Errorf("Error interpolating: %s", err) } return rendered, nil } func (s *REPLSession) handleVariables() (string, error) { varsstring := "\n" for k, v := range s.Core.Context().UserVariables { varsstring += fmt.Sprintf("%s: %+v,\n", k, v) } return varsstring, nil } func (s *REPLSession) handleHelp() (string, error) { text := ` The Packer console allows you to experiment with Packer interpolations. You may access variables in the Packer config you called the console with. Type in the interpolation to test and hit to see the result. To exit the console, type "exit" and hit , or use Control-C. ` return strings.TrimSpace(text), nil }