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