| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| 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), "<console-input>", 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 <enter> to see the result. | |
| To exit the console, type "exit" and hit <enter>, 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) | |
| } |