This commit is contained in:
Martin Atkins 2025-02-25 13:44:37 -05:00 committed by GitHub
commit b643af76dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 574 additions and 283 deletions

128
cmd/tofu/arguments.go Normal file
View File

@ -0,0 +1,128 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package main
import (
"fmt"
"log"
"os"
"strings"
shellwords "github.com/mattn/go-shellwords"
)
const (
// EnvCLI is the environment variable name to set additional CLI args.
EnvCLI = "TF_CLI_ARGS"
)
// expandFileBasedArgs searches the given args for elements whose first character is
// "@" (the "at" sign) and attempts to replace them with zero or more arguments
// parsed from a file named by the remaining characters in the string.
//
// If the remainder of the string cannot be treated as a filename for opening and
// reading then the argument is left unchanged, leaving it up to a subsequent
// codepath to decide how to treat it.
//
// The file contents are parsed using a Unix-shell-like tokenization scheme to
// produce a vector of additional arguments that will replace the original
// argument.
func expandFileBasedArgs(args []string) ([]string, error) {
// We assume that in the common case there will be no @-based arguments to
// expand and so we will wait to allocate a new slice until we know we
// need it. A nil "ret" at the end of this function therefore means that
// we should return args verbatim.
var ret []string
for i, arg := range args {
if !strings.HasPrefix(arg, "@") {
if ret != nil { // only if we've already started building a new arg vector on a previous iteration
ret = append(ret, arg)
}
continue
}
filename := arg[1:]
raw, err := os.ReadFile(filename)
if err != nil {
// We intentionally ignore errors here and just retain the
// original argument verbatim, assuming that the user intended
// to write a literal argument that starts with @.
if ret != nil { // only if we've already started building a new arg vector on a previous iteration
ret = append(ret, arg)
}
continue
}
extra, err := shellwords.Parse(string(raw))
if err != nil {
// In this case it seems more likely that the operator _was_ intending
// for this file to be treated as a set of additional arguments, but
// it contains some sort of syntax error like an unmatched opening
// quote. Therefore we'll return an error in this case to hopefully
// give the operator better feedback about the problem.
return args, fmt.Errorf("failed to expand %q argument: %w", arg, err)
}
// If we've got this far then we're definitely returning a different
// args vector than we were given, so we'll allocate it now if we
// didn't already.
if ret == nil {
// We know that we need at least enough space for all of the
// given args and the result of the one expansion we're currently
// working on, so we'll preallocate that but this might get
// reallocated by the appends below on a future iteration if
// there are multiple @-arguments.
// (-1 below because the "extra" elements are replacing our current "arg")
ret = make([]string, 0, len(args)+len(extra)-1)
ret = append(ret, args[:i]...) // start with any arguments that we already passed over
}
ret = append(ret, extra...)
}
if ret == nil {
return args, nil // We did not find any expandable arguments, so we'll return what we were given.
}
return ret, nil
}
func mergeEnvArgs(envName string, cmd string, args []string) ([]string, error) {
v := os.Getenv(envName)
if v == "" {
return args, nil
}
log.Printf("[INFO] %s value: %q", envName, v)
extra, err := shellwords.Parse(v)
if err != nil {
return nil, fmt.Errorf("error parsing extra CLI args from %s: %w", envName, err)
}
// Find the command to look for in the args. If there is a space,
// we need to find the last part.
search := cmd
if idx := strings.LastIndex(search, " "); idx >= 0 {
search = cmd[idx+1:]
}
// Find the index to place the flags. We put them exactly
// after the first non-flag arg.
idx := -1
for i, v := range args {
if v == search {
idx = i
break
}
}
// idx points to the exact arg that isn't a flag. We increment
// by one so that all the copying below expects idx to be the
// insertion point.
idx++
// Copy the args
newArgs := make([]string, len(args)+len(extra))
copy(newArgs, args[:idx])
copy(newArgs[idx:], extra)
copy(newArgs[len(extra)+idx:], args[idx:])
return newArgs, nil
}

404
cmd/tofu/arguments_test.go Normal file
View File

@ -0,0 +1,404 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package main
import (
"fmt"
"os"
"reflect"
"slices"
"testing"
"github.com/mitchellh/cli"
)
func TestExpandFileBasedArgs(t *testing.T) {
fixtureDir := "testdata/args-from-files"
tests := map[string]struct {
WantExpanded []string
WantError string
}{
"empty.txt": {
WantExpanded: []string{},
},
"escapes.txt": {
WantExpanded: []string{"can", "escape spaces", "and\nnewlines", "and", `"double`, `quotes"`, "and", "'single", "quotes'!"},
},
"unquoted.txt": {
WantExpanded: []string{"-foo", "bar", "baz"},
},
"multiline.txt": {
WantExpanded: []string{"bar", "baz", "beep", "boop"},
},
"backtick.txt": {
WantError: `failed to expand "@testdata/args-from-files/backtick.txt" argument: invalid command line string`,
},
"cmdsubst.txt": {
WantError: `failed to expand "@testdata/args-from-files/cmdsubst.txt" argument: invalid command line string`,
},
"doublequote.txt": {
WantExpanded: []string{"beep boop", "foo", "blah blah"},
},
"doublequote-envvar-like.txt": {
WantExpanded: []string{"no $INTERPOLATION inside double quotes"},
},
"doublequote-multiline.txt": {
WantExpanded: []string{"hello\nworld"},
},
"doublequote-spaces.txt": {
WantExpanded: []string{},
},
"doublequote-unclosed.txt": {
WantError: `failed to expand "@testdata/args-from-files/doublequote-unclosed.txt" argument: invalid command line string`,
},
"envvar-like.txt": {
WantExpanded: []string{"we", "$DONT", "expand", "things", "that", "look", "like", "${ENVVAR}interpolations"},
},
"parens.txt": {
// https://github.com/mattn/go-shellwords/issues/54
WantError: `failed to expand "@testdata/args-from-files/parens.txt" argument: invalid command line string`,
},
"parens-quoted.txt": {
WantExpanded: []string{"it's okay to have (parens) in double quotes", "and (also) in single quotes"},
},
"singlequote.txt": {
// This fails because the upstream library seems to mishandle double quotes inside single quotes
WantExpanded: []string{"-var=something=foo bar"},
},
"singlequote-multiline.txt": {
WantExpanded: []string{"hello\nworld"},
},
"singlequote-spaces.txt": {
WantExpanded: []string{},
},
"singlequote-unclosed.txt": {
WantError: `failed to expand "@testdata/args-from-files/singlequote-unclosed.txt" argument: invalid command line string`,
},
"nonexisting.txt": { // This intentionally refers to a filename that doesn't exist under "testdata/args-from-files"
// Apparent reference to nonexisting file is taken literally instead
WantExpanded: []string{"@testdata/args-from-files/nonexisting.txt"},
},
"unix-style-path.txt": {
WantExpanded: []string{"-var-file=../bar/baz.tfvars"},
},
"windows-style-path.txt": {
// The parsing library we use follows Unix-shell-style conventions and so backslashes
// are treated as escape characters unless in single quotes or escaped by doubling up.
WantExpanded: []string{
"-var-file=..barbaz.tfvars",
`-var-file=..\bar\baz.tfvars`,
`-var-file=..\bar\baz.tfvars`,
},
},
"metachars.txt": {
// This test fails because of https://github.com/mattn/go-shellwords/issues/57
// It currently seems to just halt parsing at the >, which is strange. I'd
// expect it to either take all of the metacharacters literally or return
// an error saying that this isn't valid syntax.
WantExpanded: []string{"not", "a", "shell,", "so", ">we", "take", "<redirection", "metacharacters", "|", "literally", "2>&but", "still", "accept", "them;", "blah"},
},
"metachars-singlequote.txt": {
WantExpanded: []string{"in quotes >we take <redirection metacharacters | literally 2>&and as a normal; part of the overall string"},
},
}
for basename, test := range tests {
t.Run(basename, func(t *testing.T) {
// NOTE: Intentionally not filepath.Join here because that would use backslashes
// on Windows but some of our WantExpanded entries want these strings to be
// taken literally and need them to be normal slashes.
filename := fixtureDir + "/" + basename
// We'll try a few different variations with the expansion argument in
// different positions with respect to other non-expanding arguments.
makeTest := func(transform func([]string) []string) func(t *testing.T) {
return func(t *testing.T) {
input := transform([]string{"@" + filename})
got, err := expandFileBasedArgs(input)
if test.WantError != "" {
if err == nil {
t.Errorf("unexpected success; want error: %s", test.WantError)
} else if gotMsg, wantMsg := err.Error(), test.WantError; gotMsg != wantMsg {
t.Errorf("wrong error\ngot: %s\nwant: %s", gotMsg, wantMsg)
}
return
}
want := make([]string, len(test.WantExpanded))
copy(want, test.WantExpanded) // copy so that the transform function can't inadvertenly modify the original backing array
want = transform(want)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !slices.Equal(got, want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want)
}
}
}
t.Run("alone", makeTest(func(s []string) []string {
return s // just the input alone, without any other arguments
}))
t.Run("before", makeTest(func(s []string) []string {
return append(s, "...")
}))
t.Run("after", makeTest(func(s []string) []string {
return append([]string{"..."}, s...)
}))
t.Run("between", makeTest(func(s []string) []string {
return append(append([]string{"..."}, s...), "...")
}))
t.Run("twice around", makeTest(func(s []string) []string {
return append(append(s, "..."), s...)
}))
})
}
// We'll also make sure we have a table entry for every file that's in
// the testdata directory, because it would be unfortunate if we had
// a test case in there that just got silently ignored.
allTestFixtures, err := os.ReadDir(fixtureDir)
if err != nil {
t.Fatalf("failed to enumerate all test fixture files: %s", err)
}
for _, entry := range allTestFixtures {
if _, ok := tests[entry.Name()]; !ok {
t.Errorf("no test case for fixture file %q", entry.Name())
}
}
}
func TestMain_cliArgsFromEnv(t *testing.T) {
// Set up the state. This test really messes with the environment and
// global state so we set things up to be restored.
// Restore original CLI args
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
// Set up test command and restore that
commands = make(map[string]cli.CommandFactory)
defer func() {
commands = nil
}()
testCommandName := "unit-test-cli-args"
testCommand := &testCommandCLI{}
commands[testCommandName] = func() (cli.Command, error) {
return testCommand, nil
}
cases := []struct {
Name string
Args []string
Value string
Expected []string
Err bool
}{
{
"no env",
[]string{testCommandName, "foo", "bar"},
"",
[]string{"foo", "bar"},
false,
},
{
"both env var and CLI",
[]string{testCommandName, "foo", "bar"},
"-foo baz",
[]string{"-foo", "baz", "foo", "bar"},
false,
},
{
"only env var",
[]string{testCommandName},
"-foo bar",
[]string{"-foo", "bar"},
false,
},
{
"cli string has blank values",
[]string{testCommandName, "bar", "", "baz"},
"-foo bar",
[]string{"-foo", "bar", "bar", "", "baz"},
false,
},
{
"cli string has blank values before the command",
[]string{"", testCommandName, "bar"},
"-foo bar",
[]string{"-foo", "bar", "bar"},
false,
},
{
// this should fail gracefully, this is just testing
// that we don't panic with our slice arithmetic
"no command",
[]string{},
"-foo bar",
nil,
true,
},
{
"single quoted strings",
[]string{testCommandName, "foo"},
"-foo 'bar baz'",
[]string{"-foo", "bar baz", "foo"},
false,
},
{
"double quoted strings",
[]string{testCommandName, "foo"},
`-foo "bar baz"`,
[]string{"-foo", "bar baz", "foo"},
false,
},
{
"double quoted single quoted strings",
[]string{testCommandName, "foo"},
`-foo "'bar baz'"`,
[]string{"-foo", "'bar baz'", "foo"},
false,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
// Set the env var value
if tc.Value != "" {
t.Setenv(EnvCLI, tc.Value)
}
// Set up the args
args := make([]string, len(tc.Args)+1)
args[0] = oldArgs[0] // process name
copy(args[1:], tc.Args)
// Run it!
os.Args = args
testCommand.Args = nil
exit := realMain()
if (exit != 0) != tc.Err {
t.Fatalf("bad: %d", exit)
}
if tc.Err {
return
}
// Verify
if !reflect.DeepEqual(testCommand.Args, tc.Expected) {
t.Fatalf("expected args %#v but got %#v", tc.Expected, testCommand.Args)
}
})
}
}
// This test just has more options than the test above. Use this for
// more control over behavior at the expense of more complex test structures.
func TestMain_cliArgsFromEnvAdvanced(t *testing.T) {
// Restore original CLI args
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
// Set up test command and restore that
commands = make(map[string]cli.CommandFactory)
defer func() {
commands = nil
}()
cases := []struct {
Name string
Command string
EnvVar string
Args []string
Value string
Expected []string
Err bool
}{
{
"targeted to another command",
"command",
EnvCLI + "_foo",
[]string{"command", "foo", "bar"},
"-flag",
[]string{"foo", "bar"},
false,
},
{
"targeted to this command",
"command",
EnvCLI + "_command",
[]string{"command", "foo", "bar"},
"-flag",
[]string{"-flag", "foo", "bar"},
false,
},
{
"targeted to a command with a hyphen",
"command-name",
EnvCLI + "_command_name",
[]string{"command-name", "foo", "bar"},
"-flag",
[]string{"-flag", "foo", "bar"},
false,
},
{
"targeted to a command with a space",
"command name",
EnvCLI + "_command_name",
[]string{"command", "name", "foo", "bar"},
"-flag",
[]string{"-flag", "foo", "bar"},
false,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
// Set up test command and restore that
testCommandName := tc.Command
testCommand := &testCommandCLI{}
defer func() { delete(commands, testCommandName) }()
commands[testCommandName] = func() (cli.Command, error) {
return testCommand, nil
}
// Set the env var value
if tc.Value != "" {
t.Setenv(tc.EnvVar, tc.Value)
}
// Set up the args
args := make([]string, len(tc.Args)+1)
args[0] = oldArgs[0] // process name
copy(args[1:], tc.Args)
// Run it!
os.Args = args
testCommand.Args = nil
exit := realMain()
if (exit != 0) != tc.Err {
t.Fatalf("unexpected exit status %d; want 0", exit)
}
if tc.Err {
return
}
// Verify
if !reflect.DeepEqual(testCommand.Args, tc.Expected) {
t.Fatalf("bad: %#v", testCommand.Args)
}
})
}
}

View File

@ -19,7 +19,6 @@ import (
"github.com/apparentlymart/go-shquot/shquot"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform-svchost/disco"
"github.com/mattn/go-shellwords"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
"github.com/opentofu/opentofu/internal/addrs"
@ -36,9 +35,6 @@ import (
)
const (
// EnvCLI is the environment variable name to set additional CLI args.
EnvCLI = "TF_CLI_ARGS"
// The parent process will create a file to collect crash logs
envTmpLogPath = "TF_TEMP_LOG_PATH"
)
@ -226,6 +222,20 @@ func realMain() int {
return 1
}
// We expand the file-based args before potentially handing the -chdir option because it's
// our existing convention to do this sort of early file access relative to the "real"
// working directory (e.g. the CLI configuration handling and reattach provider handling
// above) and because it allows the -chdir option to potentially be part of one of the
// included files.
args, err = expandFileBasedArgs(args)
if err != nil {
// expandFileBasedArgs should return an error only when it has a strong signal that
// a particular argument was intended to be expanded from a file that exists but
// the contents of that file are somehow invalid.
Ui.Error(fmt.Sprintf("Invalid command line arguments: %s", err))
return 1
}
// The arguments can begin with a -chdir option to ask OpenTofu to switch
// to a different working directory for the rest of its work. If that
// option is present then extractChdirOption returns a trimmed args with that option removed.
@ -361,50 +371,6 @@ func realMain() int {
return exitCode
}
func mergeEnvArgs(envName string, cmd string, args []string) ([]string, error) {
v := os.Getenv(envName)
if v == "" {
return args, nil
}
log.Printf("[INFO] %s value: %q", envName, v)
extra, err := shellwords.Parse(v)
if err != nil {
return nil, fmt.Errorf(
"Error parsing extra CLI args from %s: %s",
envName, err)
}
// Find the command to look for in the args. If there is a space,
// we need to find the last part.
search := cmd
if idx := strings.LastIndex(search, " "); idx >= 0 {
search = cmd[idx+1:]
}
// Find the index to place the flags. We put them exactly
// after the first non-flag arg.
idx := -1
for i, v := range args {
if v == search {
idx = i
break
}
}
// idx points to the exact arg that isn't a flag. We increment
// by one so that all the copying below expects idx to be the
// insertion point.
idx++
// Copy the args
newArgs := make([]string, len(args)+len(extra))
copy(newArgs, args[:idx])
copy(newArgs[idx:], extra)
copy(newArgs[len(extra)+idx:], args[idx:])
return newArgs, nil
}
// parse information on reattaching to unmanaged providers out of a
// JSON-encoded environment variable.
func parseReattachProviders(in string) (map[addrs.Provider]*plugin.ReattachConfig, error) {

View File

@ -9,247 +9,12 @@ import (
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"testing"
"github.com/mitchellh/cli"
)
func TestMain_cliArgsFromEnv(t *testing.T) {
// Set up the state. This test really messes with the environment and
// global state so we set things up to be restored.
// Restore original CLI args
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
// Set up test command and restore that
commands = make(map[string]cli.CommandFactory)
defer func() {
commands = nil
}()
testCommandName := "unit-test-cli-args"
testCommand := &testCommandCLI{}
commands[testCommandName] = func() (cli.Command, error) {
return testCommand, nil
}
cases := []struct {
Name string
Args []string
Value string
Expected []string
Err bool
}{
{
"no env",
[]string{testCommandName, "foo", "bar"},
"",
[]string{"foo", "bar"},
false,
},
{
"both env var and CLI",
[]string{testCommandName, "foo", "bar"},
"-foo baz",
[]string{"-foo", "baz", "foo", "bar"},
false,
},
{
"only env var",
[]string{testCommandName},
"-foo bar",
[]string{"-foo", "bar"},
false,
},
{
"cli string has blank values",
[]string{testCommandName, "bar", "", "baz"},
"-foo bar",
[]string{"-foo", "bar", "bar", "", "baz"},
false,
},
{
"cli string has blank values before the command",
[]string{"", testCommandName, "bar"},
"-foo bar",
[]string{"-foo", "bar", "bar"},
false,
},
{
// this should fail gracefully, this is just testing
// that we don't panic with our slice arithmetic
"no command",
[]string{},
"-foo bar",
nil,
true,
},
{
"single quoted strings",
[]string{testCommandName, "foo"},
"-foo 'bar baz'",
[]string{"-foo", "bar baz", "foo"},
false,
},
{
"double quoted strings",
[]string{testCommandName, "foo"},
`-foo "bar baz"`,
[]string{"-foo", "bar baz", "foo"},
false,
},
{
"double quoted single quoted strings",
[]string{testCommandName, "foo"},
`-foo "'bar baz'"`,
[]string{"-foo", "'bar baz'", "foo"},
false,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
// Set the env var value
if tc.Value != "" {
t.Setenv(EnvCLI, tc.Value)
}
// Set up the args
args := make([]string, len(tc.Args)+1)
args[0] = oldArgs[0] // process name
copy(args[1:], tc.Args)
// Run it!
os.Args = args
testCommand.Args = nil
exit := realMain()
if (exit != 0) != tc.Err {
t.Fatalf("bad: %d", exit)
}
if tc.Err {
return
}
// Verify
if !reflect.DeepEqual(testCommand.Args, tc.Expected) {
t.Fatalf("expected args %#v but got %#v", tc.Expected, testCommand.Args)
}
})
}
}
// This test just has more options than the test above. Use this for
// more control over behavior at the expense of more complex test structures.
func TestMain_cliArgsFromEnvAdvanced(t *testing.T) {
// Restore original CLI args
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
// Set up test command and restore that
commands = make(map[string]cli.CommandFactory)
defer func() {
commands = nil
}()
cases := []struct {
Name string
Command string
EnvVar string
Args []string
Value string
Expected []string
Err bool
}{
{
"targeted to another command",
"command",
EnvCLI + "_foo",
[]string{"command", "foo", "bar"},
"-flag",
[]string{"foo", "bar"},
false,
},
{
"targeted to this command",
"command",
EnvCLI + "_command",
[]string{"command", "foo", "bar"},
"-flag",
[]string{"-flag", "foo", "bar"},
false,
},
{
"targeted to a command with a hyphen",
"command-name",
EnvCLI + "_command_name",
[]string{"command-name", "foo", "bar"},
"-flag",
[]string{"-flag", "foo", "bar"},
false,
},
{
"targeted to a command with a space",
"command name",
EnvCLI + "_command_name",
[]string{"command", "name", "foo", "bar"},
"-flag",
[]string{"-flag", "foo", "bar"},
false,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
// Set up test command and restore that
testCommandName := tc.Command
testCommand := &testCommandCLI{}
defer func() { delete(commands, testCommandName) }()
commands[testCommandName] = func() (cli.Command, error) {
return testCommand, nil
}
// Set the env var value
if tc.Value != "" {
t.Setenv(tc.EnvVar, tc.Value)
}
// Set up the args
args := make([]string, len(tc.Args)+1)
args[0] = oldArgs[0] // process name
copy(args[1:], tc.Args)
// Run it!
os.Args = args
testCommand.Args = nil
exit := realMain()
if (exit != 0) != tc.Err {
t.Fatalf("unexpected exit status %d; want 0", exit)
}
if tc.Err {
return
}
// Verify
if !reflect.DeepEqual(testCommand.Args, tc.Expected) {
t.Fatalf("bad: %#v", testCommand.Args)
}
})
}
}
// verify that we output valid autocomplete results
func TestMain_autoComplete(t *testing.T) {
// Restore original CLI args

View File

@ -0,0 +1 @@
we don't expand `backtick sequences` because arbitrary command execution would be a significant security risk

View File

@ -0,0 +1 @@
we don't expand $(command substitutions) because arbitrary command execution would be a significant security risk

View File

@ -0,0 +1 @@
"no $INTERPOLATION inside double quotes"

View File

@ -0,0 +1,2 @@
"hello
world"

View File

@ -0,0 +1 @@
" "

View File

@ -0,0 +1 @@
"uh-oh

View File

@ -0,0 +1 @@
"beep boop" foo "blah blah"

View File

View File

@ -0,0 +1 @@
we $DONT expand things that look like ${ENVVAR}interpolations

View File

@ -0,0 +1,2 @@
can escape\ spaces and\
newlines and \"double quotes\" and \'single quotes\'!

View File

@ -0,0 +1 @@
'in quotes >we take <redirection metacharacters | literally 2>&and as a normal; part of the overall string'

View File

@ -0,0 +1 @@
not a shell, so >we take <redirection metacharacters | literally 2>&but still accept them; blah

View File

@ -0,0 +1,2 @@
bar baz
beep boop

View File

@ -0,0 +1,2 @@
"it's okay to have (parens) in double quotes"
'and (also) in single quotes'

View File

@ -0,0 +1 @@
we do not allow (parentheses)

View File

@ -0,0 +1,2 @@
'hello
world'

View File

@ -0,0 +1 @@
' '

View File

@ -0,0 +1 @@
'uh-oh

View File

@ -0,0 +1 @@
-var='something=foo" "bar'

View File

@ -0,0 +1 @@
-var-file=../bar/baz.tfvars

View File

@ -0,0 +1 @@
-foo bar baz

View File

@ -0,0 +1,3 @@
-var-file=..\bar\baz.tfvars
-var-file='..\bar\baz.tfvars'
-var-file=..\\bar\\baz.tfvars