mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-25 08:21:07 -06:00
209 lines
5.9 KiB
Go
209 lines
5.9 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
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/opentofu/opentofu/internal/lang"
|
|
"github.com/opentofu/opentofu/internal/lang/marks"
|
|
"github.com/opentofu/opentofu/internal/lang/types"
|
|
"github.com/opentofu/opentofu/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 OpenTofu console allows you to experiment with OpenTofu 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)
|
|
}
|