package repl import ( "fmt" "sort" "strings" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/lang/types" "github.com/hashicorp/terraform/internal/tfdiags" ) // Session represents the state for a single REPL session. type Session struct { // Scope is the evaluation scope where expressions will be evaluated. Scope *lang.Scope } // Handle handles a single line of input from the REPL. // // This is a stateful operation if a command is given (such as setting // a variable). This function should not be called in parallel. // // The return value is the output and the error to show. func (s *Session) Handle(line string) (string, bool, tfdiags.Diagnostics) { switch { case strings.TrimSpace(line) == "": return "", false, nil case strings.TrimSpace(line) == "exit": return "", true, nil case strings.TrimSpace(line) == "help": ret, diags := s.handleHelp() return ret, false, diags default: ret, diags := s.handleEval(line) return ret, false, diags } } func (s *Session) handleEval(line string) (string, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // Parse the given line as an expression expr, parseDiags := hclsyntax.ParseExpression([]byte(line), "", hcl.Pos{Line: 1, Column: 1}) diags = diags.Append(parseDiags) if parseDiags.HasErrors() { return "", diags } val, valDiags := s.Scope.EvalExpr(expr, cty.DynamicPseudoType) diags = diags.Append(valDiags) if valDiags.HasErrors() { return "", diags } // The TypeType mark is used only by the console-only `type` function, in // order to smuggle the type of a given value back here. We can then // display a representation of the type directly. if marks.Contains(val, marks.TypeType) { val, _ = val.UnmarkDeep() valType := val.Type() switch { case valType.Equals(types.TypeType): // An encapsulated type value, which should be displayed directly. valType := val.EncapsulatedValue().(*cty.Type) return typeString(*valType), diags default: diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid use of type function", "The console-only \"type\" function cannot be used as part of an expression.", )) return "", diags } } return FormatValue(val, 0), diags } func (s *Session) handleHelp() (string, tfdiags.Diagnostics) { text := ` The Terraform console allows you to experiment with Terraform interpolations. You may access resources in the state (if you have one) just as you would from a configuration. For example: "aws_instance.foo.id" would evaluate to the ID of "aws_instance.foo" if it exists in your state. Type in the interpolation to test and hit to see the result. To exit the console, type "exit" and hit , or use Control-C or Control-D. ` return strings.TrimSpace(text), nil } // Modified copy of TypeString from go-cty: // https://github.com/zclconf/go-cty-debug/blob/master/ctydebug/type_string.go // // TypeString returns a string representation of a given type that is // reminiscent of Go syntax calling into the cty package but is mainly // intended for easy human inspection of values in tests, debug output, etc. // // The resulting string will include newlines and indentation in order to // increase the readability of complex structures. It always ends with a // newline, so you can print this result directly to your output. func typeString(ty cty.Type) string { var b strings.Builder writeType(ty, &b, 0) return b.String() } func writeType(ty cty.Type, b *strings.Builder, indent int) { switch { case ty == cty.NilType: b.WriteString("nil") return case ty.IsObjectType(): atys := ty.AttributeTypes() if len(atys) == 0 { b.WriteString("object({})") return } attrNames := make([]string, 0, len(atys)) for name := range atys { attrNames = append(attrNames, name) } sort.Strings(attrNames) b.WriteString("object({\n") indent++ for _, name := range attrNames { aty := atys[name] b.WriteString(indentSpaces(indent)) fmt.Fprintf(b, "%s: ", name) writeType(aty, b, indent) b.WriteString(",\n") } indent-- b.WriteString(indentSpaces(indent)) b.WriteString("})") case ty.IsTupleType(): etys := ty.TupleElementTypes() if len(etys) == 0 { b.WriteString("tuple([])") return } b.WriteString("tuple([\n") indent++ for _, ety := range etys { b.WriteString(indentSpaces(indent)) writeType(ety, b, indent) b.WriteString(",\n") } indent-- b.WriteString(indentSpaces(indent)) b.WriteString("])") case ty.IsCollectionType(): ety := ty.ElementType() switch { case ty.IsListType(): b.WriteString("list(") case ty.IsMapType(): b.WriteString("map(") case ty.IsSetType(): b.WriteString("set(") default: // At the time of writing there are no other collection types, // but we'll be robust here and just pass through the GoString // of anything we don't recognize. b.WriteString(ty.FriendlyName()) return } // Because object and tuple types render split over multiple // lines, a collection type container around them can end up // being hard to see when scanning, so we'll generate some extra // indentation to make a collection of structural type more visually // distinct from the structural type alone. complexElem := ety.IsObjectType() || ety.IsTupleType() if complexElem { indent++ b.WriteString("\n") b.WriteString(indentSpaces(indent)) } writeType(ty.ElementType(), b, indent) if complexElem { indent-- b.WriteString(",\n") b.WriteString(indentSpaces(indent)) } b.WriteString(")") default: // For any other type we'll just use its GoString and assume it'll // follow the usual GoString conventions. b.WriteString(ty.FriendlyName()) } } func indentSpaces(level int) string { return strings.Repeat(" ", level) }