github.com/opentofu/opentofu@v1.7.1/internal/repl/session.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package repl 7 8 import ( 9 "fmt" 10 "sort" 11 "strings" 12 13 "github.com/zclconf/go-cty/cty" 14 15 "github.com/hashicorp/hcl/v2" 16 "github.com/hashicorp/hcl/v2/hclsyntax" 17 "github.com/opentofu/opentofu/internal/lang" 18 "github.com/opentofu/opentofu/internal/lang/marks" 19 "github.com/opentofu/opentofu/internal/lang/types" 20 "github.com/opentofu/opentofu/internal/tfdiags" 21 ) 22 23 // Session represents the state for a single REPL session. 24 type Session struct { 25 // Scope is the evaluation scope where expressions will be evaluated. 26 Scope *lang.Scope 27 } 28 29 // Handle handles a single line of input from the REPL. 30 // 31 // This is a stateful operation if a command is given (such as setting 32 // a variable). This function should not be called in parallel. 33 // 34 // The return value is the output and the error to show. 35 func (s *Session) Handle(line string) (string, bool, tfdiags.Diagnostics) { 36 switch { 37 case strings.TrimSpace(line) == "": 38 return "", false, nil 39 case strings.TrimSpace(line) == "exit": 40 return "", true, nil 41 case strings.TrimSpace(line) == "help": 42 ret, diags := s.handleHelp() 43 return ret, false, diags 44 default: 45 ret, diags := s.handleEval(line) 46 return ret, false, diags 47 } 48 } 49 50 func (s *Session) handleEval(line string) (string, tfdiags.Diagnostics) { 51 var diags tfdiags.Diagnostics 52 53 // Parse the given line as an expression 54 expr, parseDiags := hclsyntax.ParseExpression([]byte(line), "<console-input>", hcl.Pos{Line: 1, Column: 1}) 55 diags = diags.Append(parseDiags) 56 if parseDiags.HasErrors() { 57 return "", diags 58 } 59 60 val, valDiags := s.Scope.EvalExpr(expr, cty.DynamicPseudoType) 61 diags = diags.Append(valDiags) 62 if valDiags.HasErrors() { 63 return "", diags 64 } 65 66 // The TypeType mark is used only by the console-only `type` function, in 67 // order to smuggle the type of a given value back here. We can then 68 // display a representation of the type directly. 69 if marks.Contains(val, marks.TypeType) { 70 val, _ = val.UnmarkDeep() 71 72 valType := val.Type() 73 switch { 74 case valType.Equals(types.TypeType): 75 // An encapsulated type value, which should be displayed directly. 76 valType := val.EncapsulatedValue().(*cty.Type) 77 return typeString(*valType), diags 78 default: 79 diags = diags.Append(tfdiags.Sourceless( 80 tfdiags.Error, 81 "Invalid use of type function", 82 "The console-only \"type\" function cannot be used as part of an expression.", 83 )) 84 return "", diags 85 } 86 } 87 88 return FormatValue(val, 0), diags 89 } 90 91 func (s *Session) handleHelp() (string, tfdiags.Diagnostics) { 92 text := ` 93 The OpenTofu console allows you to experiment with OpenTofu interpolations. 94 You may access resources in the state (if you have one) just as you would 95 from a configuration. For example: "aws_instance.foo.id" would evaluate 96 to the ID of "aws_instance.foo" if it exists in your state. 97 98 Type in the interpolation to test and hit <enter> to see the result. 99 100 To exit the console, type "exit" and hit <enter>, or use Control-C or 101 Control-D. 102 ` 103 104 return strings.TrimSpace(text), nil 105 } 106 107 // Modified copy of TypeString from go-cty: 108 // https://github.com/zclconf/go-cty-debug/blob/master/ctydebug/type_string.go 109 // 110 // TypeString returns a string representation of a given type that is 111 // reminiscent of Go syntax calling into the cty package but is mainly 112 // intended for easy human inspection of values in tests, debug output, etc. 113 // 114 // The resulting string will include newlines and indentation in order to 115 // increase the readability of complex structures. It always ends with a 116 // newline, so you can print this result directly to your output. 117 func typeString(ty cty.Type) string { 118 var b strings.Builder 119 writeType(ty, &b, 0) 120 return b.String() 121 } 122 123 func writeType(ty cty.Type, b *strings.Builder, indent int) { 124 switch { 125 case ty == cty.NilType: 126 b.WriteString("nil") 127 return 128 case ty.IsObjectType(): 129 atys := ty.AttributeTypes() 130 if len(atys) == 0 { 131 b.WriteString("object({})") 132 return 133 } 134 attrNames := make([]string, 0, len(atys)) 135 for name := range atys { 136 attrNames = append(attrNames, name) 137 } 138 sort.Strings(attrNames) 139 b.WriteString("object({\n") 140 indent++ 141 for _, name := range attrNames { 142 aty := atys[name] 143 b.WriteString(indentSpaces(indent)) 144 fmt.Fprintf(b, "%s: ", name) 145 writeType(aty, b, indent) 146 b.WriteString(",\n") 147 } 148 indent-- 149 b.WriteString(indentSpaces(indent)) 150 b.WriteString("})") 151 case ty.IsTupleType(): 152 etys := ty.TupleElementTypes() 153 if len(etys) == 0 { 154 b.WriteString("tuple([])") 155 return 156 } 157 b.WriteString("tuple([\n") 158 indent++ 159 for _, ety := range etys { 160 b.WriteString(indentSpaces(indent)) 161 writeType(ety, b, indent) 162 b.WriteString(",\n") 163 } 164 indent-- 165 b.WriteString(indentSpaces(indent)) 166 b.WriteString("])") 167 case ty.IsCollectionType(): 168 ety := ty.ElementType() 169 switch { 170 case ty.IsListType(): 171 b.WriteString("list(") 172 case ty.IsMapType(): 173 b.WriteString("map(") 174 case ty.IsSetType(): 175 b.WriteString("set(") 176 default: 177 // At the time of writing there are no other collection types, 178 // but we'll be robust here and just pass through the GoString 179 // of anything we don't recognize. 180 b.WriteString(ty.FriendlyName()) 181 return 182 } 183 // Because object and tuple types render split over multiple 184 // lines, a collection type container around them can end up 185 // being hard to see when scanning, so we'll generate some extra 186 // indentation to make a collection of structural type more visually 187 // distinct from the structural type alone. 188 complexElem := ety.IsObjectType() || ety.IsTupleType() 189 if complexElem { 190 indent++ 191 b.WriteString("\n") 192 b.WriteString(indentSpaces(indent)) 193 } 194 writeType(ty.ElementType(), b, indent) 195 if complexElem { 196 indent-- 197 b.WriteString(",\n") 198 b.WriteString(indentSpaces(indent)) 199 } 200 b.WriteString(")") 201 default: 202 // For any other type we'll just use its GoString and assume it'll 203 // follow the usual GoString conventions. 204 b.WriteString(ty.FriendlyName()) 205 } 206 } 207 208 func indentSpaces(level int) string { 209 return strings.Repeat(" ", level) 210 }