mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Adds multi line support to console (#1875)
Signed-off-by: Andrew Hayes <andrew.hayes@harness.io>
This commit is contained in:
parent
1c0cb13bf7
commit
131c2cadda
@ -8,6 +8,7 @@ ENHANCEMENTS:
|
|||||||
* Added `-show-sensitive` flag to tofu plan, apply, state-show and output commands to display sensitive data in output. ([#1554](https://github.com/opentofu/opentofu/pull/1554))
|
* Added `-show-sensitive` flag to tofu plan, apply, state-show and output commands to display sensitive data in output. ([#1554](https://github.com/opentofu/opentofu/pull/1554))
|
||||||
* Improved performance for large graphs when debug logs are not enabled. ([#1810](https://github.com/opentofu/opentofu/pull/1810))
|
* Improved performance for large graphs when debug logs are not enabled. ([#1810](https://github.com/opentofu/opentofu/pull/1810))
|
||||||
* Improved performance for large graphs with many submodules. ([#1809](https://github.com/opentofu/opentofu/pull/1809))
|
* Improved performance for large graphs with many submodules. ([#1809](https://github.com/opentofu/opentofu/pull/1809))
|
||||||
|
* Added mutli-line support to the `tofu console` command. ([#1307](https://github.com/opentofu/opentofu/issues/1307))
|
||||||
|
|
||||||
BUG FIXES:
|
BUG FIXES:
|
||||||
* Ensure that using a sensitive path for templatefile that it doesn't panic([#1801](https://github.com/opentofu/opentofu/issues/1801))
|
* Ensure that using a sensitive path for templatefile that it doesn't panic([#1801](https://github.com/opentofu/opentofu/issues/1801))
|
||||||
|
@ -179,26 +179,31 @@ func (c *ConsoleCommand) Run(args []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConsoleCommand) modePiped(session *repl.Session, ui cli.Ui) int {
|
func (c *ConsoleCommand) modePiped(session *repl.Session, ui cli.Ui) int {
|
||||||
var lastResult string
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
|
||||||
|
var consoleState consoleBracketState
|
||||||
|
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
result, exit, diags := session.Handle(strings.TrimSpace(scanner.Text()))
|
line := strings.TrimSpace(scanner.Text())
|
||||||
if diags.HasErrors() {
|
|
||||||
// In piped mode we'll exit immediately on error.
|
|
||||||
c.showDiagnostics(diags)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if exit {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the last result
|
// we check if there is no escaped new line at the end, or any open brackets
|
||||||
lastResult = result
|
// if we have neither, then we can execute
|
||||||
|
fullCommand, bracketState := consoleState.UpdateState(line)
|
||||||
|
if bracketState <= 0 {
|
||||||
|
result, exit, diags := session.Handle(fullCommand)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
// We're in piped mode, so we'll exit immediately on error.
|
||||||
|
c.showDiagnostics(diags)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if exit {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// Output the result
|
||||||
|
ui.Output(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output the final result
|
|
||||||
ui.Output(lastResult)
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,9 +6,11 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/opentofu/opentofu/internal/repl"
|
"github.com/opentofu/opentofu/internal/repl"
|
||||||
|
|
||||||
@ -35,28 +37,46 @@ func (c *ConsoleCommand) modeInteractive(session *repl.Session, ui cli.Ui) int {
|
|||||||
}
|
}
|
||||||
defer l.Close()
|
defer l.Close()
|
||||||
|
|
||||||
|
var consoleState consoleBracketState
|
||||||
|
|
||||||
for {
|
for {
|
||||||
// Read a line
|
// Read a line
|
||||||
line, err := l.Readline()
|
line, err := l.Readline()
|
||||||
if err == readline.ErrInterrupt {
|
if errors.Is(err, readline.ErrInterrupt) {
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else if err == io.EOF {
|
} else if errors.Is(err, io.EOF) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
|
||||||
out, exit, diags := session.Handle(line)
|
// we update the state with the new line, so if we have open
|
||||||
if diags.HasErrors() {
|
// brackets we know not to execute the command just yet
|
||||||
c.showDiagnostics(diags)
|
fullCommand, openState := consoleState.UpdateState(line)
|
||||||
}
|
|
||||||
if exit {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.Output(out)
|
switch {
|
||||||
|
case openState > 0:
|
||||||
|
// here there are open brackets somewhere, so we don't execute it
|
||||||
|
// as we are in a bracket we update the prompt. we use one . per layer pf brackets
|
||||||
|
l.SetPrompt(fmt.Sprintf("%s ", strings.Repeat(".", openState)))
|
||||||
|
default:
|
||||||
|
out, exit, diags := session.Handle(fullCommand)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
c.showDiagnostics(diags)
|
||||||
|
}
|
||||||
|
if exit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear the state and buffer as we have executed a command
|
||||||
|
// we also reset the prompt
|
||||||
|
l.SetPrompt("> ")
|
||||||
|
|
||||||
|
ui.Output(out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
121
internal/command/console_interactive_test.go
Normal file
121
internal/command/console_interactive_test.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
// Copyright (c) The OpenTofu Authors
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
// Copyright (c) 2023 HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||||
|
"github.com/opentofu/opentofu/internal/providers"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConsole_multiline_interactive(t *testing.T) {
|
||||||
|
td := t.TempDir()
|
||||||
|
testCopyDir(t, testFixturePath("console-multiline-vars"), td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
||||||
|
ResourceTypes: map[string]providers.Schema{
|
||||||
|
"test_instance": {
|
||||||
|
Block: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"value": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ui := cli.NewMockUi()
|
||||||
|
view, _ := testView(t)
|
||||||
|
c := &ConsoleCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
testingOverrides: metaOverridesForProvider(p),
|
||||||
|
Ui: ui,
|
||||||
|
View: view,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := map[string]testCase{
|
||||||
|
"single_line": {
|
||||||
|
input: `var.counts.lalala`,
|
||||||
|
expected: "1\n",
|
||||||
|
},
|
||||||
|
"basic_multi_line": {
|
||||||
|
input: `
|
||||||
|
var.counts.lalala
|
||||||
|
var.counts.lololo`,
|
||||||
|
expected: "\n1\n2\n",
|
||||||
|
},
|
||||||
|
"backets_multi_line": {
|
||||||
|
input: `
|
||||||
|
var.counts.lalala
|
||||||
|
split(
|
||||||
|
"_",
|
||||||
|
"lalala_lolol_lelelele"
|
||||||
|
)`,
|
||||||
|
expected: "\n1\ntolist([\n \"lalala\",\n \"lolol\",\n \"lelelele\",\n])\n",
|
||||||
|
},
|
||||||
|
"baces_multi_line": {
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
for key, value in var.counts : key => value
|
||||||
|
if value == 1
|
||||||
|
}`,
|
||||||
|
expected: "\n{\n \"lalala\" = 1\n}\n",
|
||||||
|
},
|
||||||
|
"escaped_new_line": {
|
||||||
|
input: `
|
||||||
|
5 + 4 \
|
||||||
|
|
||||||
|
`,
|
||||||
|
expected: "\n9\n\n",
|
||||||
|
},
|
||||||
|
"heredoc": {
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
default = <<-EOT
|
||||||
|
lulululu
|
||||||
|
EOT
|
||||||
|
}`,
|
||||||
|
expected: "\n{\n \"default\" = <<-EOT\n lulululu\n \n EOT\n}\n",
|
||||||
|
},
|
||||||
|
"quoted_braces": {
|
||||||
|
input: "{\ndefault = format(\"%s%s%s\",\"{\",var.counts.lalala,\"}\")\n}",
|
||||||
|
expected: "{\n \"default\" = \"{1}\"\n}\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for testName, tc := range tests {
|
||||||
|
t.Run(testName, func(t *testing.T) {
|
||||||
|
var output bytes.Buffer
|
||||||
|
defer testStdinPipe(t, strings.NewReader(tc.input))()
|
||||||
|
outCloser := testStdoutCapture(t, &output)
|
||||||
|
|
||||||
|
args := []string{}
|
||||||
|
code := c.Run(args)
|
||||||
|
outCloser()
|
||||||
|
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
got := output.String()
|
||||||
|
if got != tc.expected {
|
||||||
|
t.Fatalf("unexpected output. For input: %s\ngot: %q\nexpected: %q", tc.input, got, tc.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
101
internal/command/console_state.go
Normal file
101
internal/command/console_state.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// Copyright (c) The OpenTofu Authors
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
// Copyright (c) 2023 HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||||
|
)
|
||||||
|
|
||||||
|
type consoleBracketState struct {
|
||||||
|
openNewLine int
|
||||||
|
brace int
|
||||||
|
bracket int
|
||||||
|
parentheses int
|
||||||
|
buffer []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// commandInOpenState return an int to inform if brackets are open
|
||||||
|
// or if any escaped new lines
|
||||||
|
// in the console and we should hold off on processing the commands
|
||||||
|
// it returns 3 states:
|
||||||
|
// -1 is returned the is an incorrect amount of brackets.
|
||||||
|
// for example "())" has too many close brackets
|
||||||
|
// 0 is returned if the brackets are closed.
|
||||||
|
// for examples "()" or "" would be in a close bracket state
|
||||||
|
// >=1 is returned for the amount of open brackets.
|
||||||
|
// for example "({" would return 2. "({}" would return 1
|
||||||
|
func (c *consoleBracketState) commandInOpenState() int {
|
||||||
|
switch {
|
||||||
|
case c.brace < 0:
|
||||||
|
fallthrough
|
||||||
|
case c.bracket < 0:
|
||||||
|
fallthrough
|
||||||
|
case c.parentheses < 0:
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// we calculate open brackets, braces and parentheses by the diff between each count
|
||||||
|
var total int
|
||||||
|
total += c.openNewLine
|
||||||
|
total += c.brace
|
||||||
|
total += c.bracket
|
||||||
|
total += c.parentheses
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateState updates the state of the console with the latest line data
|
||||||
|
func (c *consoleBracketState) UpdateState(line string) (string, int) {
|
||||||
|
defer c.checkStateAndClearBuffer()
|
||||||
|
// as new lines are a kind of "one off" we reset each update
|
||||||
|
c.openNewLine = 0
|
||||||
|
|
||||||
|
// escaped new lines are treated as a "one off" bracket
|
||||||
|
// the four \\\\ means we have a false positive for a new line, as it's just an escaped \..
|
||||||
|
if strings.HasSuffix(line, "\\") && !strings.HasSuffix(line, "\\\\") {
|
||||||
|
c.openNewLine++
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.TrimSuffix(line, "\\")
|
||||||
|
if len(line) == 0 {
|
||||||
|
// we can skip empty lines
|
||||||
|
return c.getCommand(), c.commandInOpenState()
|
||||||
|
}
|
||||||
|
c.buffer = append(c.buffer, line)
|
||||||
|
|
||||||
|
tokens, _ := hclsyntax.LexConfig([]byte(line), "<console-input>", hcl.Pos{Line: 1, Column: 1})
|
||||||
|
for _, token := range tokens {
|
||||||
|
switch token.Type { //nolint:exhaustive // we only care about these specific types
|
||||||
|
case hclsyntax.TokenOBrace:
|
||||||
|
c.brace++
|
||||||
|
case hclsyntax.TokenCBrace:
|
||||||
|
c.brace--
|
||||||
|
case hclsyntax.TokenOBrack:
|
||||||
|
c.bracket++
|
||||||
|
case hclsyntax.TokenCBrack:
|
||||||
|
c.bracket--
|
||||||
|
case hclsyntax.TokenOParen:
|
||||||
|
c.parentheses++
|
||||||
|
case hclsyntax.TokenCParen:
|
||||||
|
c.parentheses--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.getCommand(), c.commandInOpenState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCommand joins the buffer and returns it
|
||||||
|
func (c *consoleBracketState) getCommand() string {
|
||||||
|
output := strings.Join(c.buffer, "\n")
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *consoleBracketState) checkStateAndClearBuffer() {
|
||||||
|
if c.commandInOpenState() <= 0 {
|
||||||
|
c.buffer = []string{}
|
||||||
|
}
|
||||||
|
}
|
226
internal/command/console_state_test.go
Normal file
226
internal/command/console_state_test.go
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
// Copyright (c) The OpenTofu Authors
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
// Copyright (c) 2023 HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package command
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func Test_commandInOpenState(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
input string
|
||||||
|
expected int
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := map[string]testCase{
|
||||||
|
"plain braces": {
|
||||||
|
input: "{}",
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
"plain brackets": {
|
||||||
|
input: "[]",
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
"plain parentheses": {
|
||||||
|
input: "()",
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
"open braces": {
|
||||||
|
input: "{",
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
"open brackets": {
|
||||||
|
input: "[",
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
"open parentheses": {
|
||||||
|
input: "(",
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
"two open braces": {
|
||||||
|
input: "{{",
|
||||||
|
expected: 2,
|
||||||
|
},
|
||||||
|
"two open brackets": {
|
||||||
|
input: "[[",
|
||||||
|
expected: 2,
|
||||||
|
},
|
||||||
|
"two open parentheses": {
|
||||||
|
input: "((",
|
||||||
|
expected: 2,
|
||||||
|
},
|
||||||
|
"open and closed braces": {
|
||||||
|
input: "{{}",
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
"open and closed brackets": {
|
||||||
|
input: "[[]",
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
"open and closed parentheses": {
|
||||||
|
input: "(()",
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
"mix braces and brackets": {
|
||||||
|
input: "{[]",
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
"mix brackets and parentheses": {
|
||||||
|
input: "[()",
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
"mix parentheses and braces": {
|
||||||
|
input: "({}",
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
"invalid braces": {
|
||||||
|
input: "{}}",
|
||||||
|
expected: -1,
|
||||||
|
},
|
||||||
|
"invalid brackets": {
|
||||||
|
input: "[]]",
|
||||||
|
expected: -1,
|
||||||
|
},
|
||||||
|
"invalid parentheses": {
|
||||||
|
input: "())",
|
||||||
|
expected: -1,
|
||||||
|
},
|
||||||
|
"escaped new line": {
|
||||||
|
input: "\\",
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
"false positive new line": {
|
||||||
|
input: "\\\\",
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
"mix parentheses and new line": {
|
||||||
|
input: "(\\",
|
||||||
|
expected: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for testName, tc := range tests {
|
||||||
|
t.Run(testName, func(t *testing.T) {
|
||||||
|
state := consoleBracketState{}
|
||||||
|
_, actual := state.UpdateState(tc.input)
|
||||||
|
if actual != tc.expected {
|
||||||
|
t.Fatalf("Actual: %d, expected %d", actual, tc.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_UpdateState(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
inputs []string
|
||||||
|
expected int
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := map[string]testCase{
|
||||||
|
"plain braces": {
|
||||||
|
inputs: []string{"{", "}"},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
"open brackets": {
|
||||||
|
inputs: []string{"[", "[", "]"},
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
"invalid parenthesis": {
|
||||||
|
inputs: []string{"(", ")", ")"},
|
||||||
|
expected: -1,
|
||||||
|
},
|
||||||
|
"a fake brace": {
|
||||||
|
inputs: []string{"{", "\"}\"", "}"},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
"a mixed bag": {
|
||||||
|
inputs: []string{"{", "}", "[", "...", "()", "]"},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
"multiple open": {
|
||||||
|
inputs: []string{"{", "[", "("},
|
||||||
|
expected: 3,
|
||||||
|
},
|
||||||
|
"escaped new line": {
|
||||||
|
inputs: []string{"\\"},
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
"false positive new line": {
|
||||||
|
inputs: []string{"\\\\"},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for testName, tc := range tests {
|
||||||
|
t.Run(testName, func(t *testing.T) {
|
||||||
|
actual := 0
|
||||||
|
state := consoleBracketState{}
|
||||||
|
for _, input := range tc.inputs {
|
||||||
|
_, actual = state.UpdateState(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
if actual != tc.expected {
|
||||||
|
t.Fatalf("Actual: %d, expected %d", actual, tc.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GetFullCommand(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
inputs []string
|
||||||
|
expected []string
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := map[string]testCase{
|
||||||
|
"plain braces": {
|
||||||
|
inputs: []string{"{", "}"},
|
||||||
|
expected: []string{"{", "{\n}"},
|
||||||
|
},
|
||||||
|
"open brackets": {
|
||||||
|
inputs: []string{"[", "[", "]"},
|
||||||
|
expected: []string{"[", "[\n[", "[\n[\n]"},
|
||||||
|
},
|
||||||
|
"invalid parenthesis": {
|
||||||
|
inputs: []string{"(", ")", ")"},
|
||||||
|
expected: []string{"(", "(\n)", ")"},
|
||||||
|
},
|
||||||
|
"a fake brace": {
|
||||||
|
inputs: []string{"{", "\"}\"", "}"},
|
||||||
|
expected: []string{"{", "{\n\"}\"", "{\n\"}\"\n}"},
|
||||||
|
},
|
||||||
|
"a mixed bag": {
|
||||||
|
inputs: []string{"{", "}", "[", "...", "", "()", "]"},
|
||||||
|
expected: []string{"{", "{\n}", "[", "[\n...", "[\n...", "[\n...\n()", "[\n...\n()\n]"},
|
||||||
|
},
|
||||||
|
"multiple open": {
|
||||||
|
inputs: []string{"{", "[", "("},
|
||||||
|
expected: []string{"{", "{\n[", "{\n[\n("},
|
||||||
|
},
|
||||||
|
"escaped new line": {
|
||||||
|
inputs: []string{"\\"},
|
||||||
|
expected: []string{""},
|
||||||
|
},
|
||||||
|
"false positive new line": {
|
||||||
|
inputs: []string{"\\\\"},
|
||||||
|
expected: []string{"\\"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for testName, tc := range tests {
|
||||||
|
t.Run(testName, func(t *testing.T) {
|
||||||
|
state := consoleBracketState{}
|
||||||
|
if len(tc.inputs) != len(tc.expected) {
|
||||||
|
t.Fatalf("\nthe length of inputs: %d\n and expected: %d don't match", len(tc.inputs), len(tc.expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, input := range tc.inputs {
|
||||||
|
actual, _ := state.UpdateState(input)
|
||||||
|
if actual != tc.expected[i] {
|
||||||
|
t.Fatalf("\nActual: %q\nexpected: %q", actual, tc.expected[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||||
"github.com/opentofu/opentofu/internal/providers"
|
"github.com/opentofu/opentofu/internal/providers"
|
||||||
|
"github.com/opentofu/opentofu/internal/terminal"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -241,3 +242,111 @@ func TestConsole_modules(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConsole_multiline_pipe(t *testing.T) {
|
||||||
|
td := t.TempDir()
|
||||||
|
testCopyDir(t, testFixturePath("console-multiline-vars"), td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
||||||
|
ResourceTypes: map[string]providers.Schema{
|
||||||
|
"test_instance": {
|
||||||
|
Block: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"value": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := map[string]testCase{
|
||||||
|
"single_line": {
|
||||||
|
input: `var.counts.lalala`,
|
||||||
|
expected: "1\n",
|
||||||
|
},
|
||||||
|
"basic_multi_line": {
|
||||||
|
input: `
|
||||||
|
var.counts.lalala
|
||||||
|
var.counts.lololo`,
|
||||||
|
expected: "\n1\n2\n",
|
||||||
|
},
|
||||||
|
"backets_multi_line": {
|
||||||
|
input: `
|
||||||
|
var.counts.lalala
|
||||||
|
split(
|
||||||
|
"_",
|
||||||
|
"lalala_lolol_lelelele"
|
||||||
|
)`,
|
||||||
|
expected: "\n1\ntolist([\n \"lalala\",\n \"lolol\",\n \"lelelele\",\n])\n",
|
||||||
|
},
|
||||||
|
"baces_multi_line": {
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
for key, value in var.counts : key => value
|
||||||
|
if value == 1
|
||||||
|
}`,
|
||||||
|
expected: "\n{\n \"lalala\" = 1\n}\n",
|
||||||
|
},
|
||||||
|
"escaped_new_line": {
|
||||||
|
input: `
|
||||||
|
5 + 4 \
|
||||||
|
|
||||||
|
`,
|
||||||
|
expected: "\n9\n\n",
|
||||||
|
},
|
||||||
|
"heredoc": {
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
default = <<-EOT
|
||||||
|
lulululu
|
||||||
|
EOT
|
||||||
|
}`,
|
||||||
|
expected: "\n{\n \"default\" = <<-EOT\n lulululu\n \n EOT\n}\n",
|
||||||
|
},
|
||||||
|
"quoted_braces": {
|
||||||
|
input: "{\ndefault = format(\"%s%s%s\",\"{\",var.counts.lalala,\"}\")\n}",
|
||||||
|
expected: "{\n \"default\" = \"{1}\"\n}\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for testName, tc := range tests {
|
||||||
|
t.Run(testName, func(t *testing.T) {
|
||||||
|
streams, _ := terminal.StreamsForTesting(t)
|
||||||
|
|
||||||
|
ui := cli.NewMockUi()
|
||||||
|
view, _ := testView(t)
|
||||||
|
c := &ConsoleCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
testingOverrides: metaOverridesForProvider(p),
|
||||||
|
Ui: ui,
|
||||||
|
View: view,
|
||||||
|
Streams: streams,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var output bytes.Buffer
|
||||||
|
defer testStdinPipe(t, strings.NewReader(tc.input))()
|
||||||
|
outCloser := testStdoutCapture(t, &output)
|
||||||
|
|
||||||
|
args := []string{}
|
||||||
|
code := c.Run(args)
|
||||||
|
outCloser()
|
||||||
|
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
got := output.String()
|
||||||
|
if got != tc.expected {
|
||||||
|
t.Fatalf("unexpected output\ngot: %q\nexpected: %q", got, tc.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
13
internal/command/testdata/console-multiline-vars/main.tf
vendored
Normal file
13
internal/command/testdata/console-multiline-vars/main.tf
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
variable "bar" {
|
||||||
|
default = "baz"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "foo" {}
|
||||||
|
|
||||||
|
variable "counts" {
|
||||||
|
type = map(any)
|
||||||
|
default = {
|
||||||
|
"lalala" = 1,
|
||||||
|
"lololo" = 2,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user