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  }