mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Merge 0e1fcb1bf7
into eba25e2fed
This commit is contained in:
commit
b643af76dc
128
cmd/tofu/arguments.go
Normal file
128
cmd/tofu/arguments.go
Normal 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
404
cmd/tofu/arguments_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
|
1
cmd/tofu/testdata/args-from-files/backtick.txt
vendored
Normal file
1
cmd/tofu/testdata/args-from-files/backtick.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
we don't expand `backtick sequences` because arbitrary command execution would be a significant security risk
|
1
cmd/tofu/testdata/args-from-files/cmdsubst.txt
vendored
Normal file
1
cmd/tofu/testdata/args-from-files/cmdsubst.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
we don't expand $(command substitutions) because arbitrary command execution would be a significant security risk
|
1
cmd/tofu/testdata/args-from-files/doublequote-envvar-like.txt
vendored
Normal file
1
cmd/tofu/testdata/args-from-files/doublequote-envvar-like.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
"no $INTERPOLATION inside double quotes"
|
2
cmd/tofu/testdata/args-from-files/doublequote-multiline.txt
vendored
Normal file
2
cmd/tofu/testdata/args-from-files/doublequote-multiline.txt
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
"hello
|
||||
world"
|
1
cmd/tofu/testdata/args-from-files/doublequote-spaces.txt
vendored
Normal file
1
cmd/tofu/testdata/args-from-files/doublequote-spaces.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
" "
|
1
cmd/tofu/testdata/args-from-files/doublequote-unclosed.txt
vendored
Normal file
1
cmd/tofu/testdata/args-from-files/doublequote-unclosed.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
"uh-oh
|
1
cmd/tofu/testdata/args-from-files/doublequote.txt
vendored
Normal file
1
cmd/tofu/testdata/args-from-files/doublequote.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
"beep boop" foo "blah blah"
|
0
cmd/tofu/testdata/args-from-files/empty.txt
vendored
Normal file
0
cmd/tofu/testdata/args-from-files/empty.txt
vendored
Normal file
1
cmd/tofu/testdata/args-from-files/envvar-like.txt
vendored
Normal file
1
cmd/tofu/testdata/args-from-files/envvar-like.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
we $DONT expand things that look like ${ENVVAR}interpolations
|
2
cmd/tofu/testdata/args-from-files/escapes.txt
vendored
Normal file
2
cmd/tofu/testdata/args-from-files/escapes.txt
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
can escape\ spaces and\
|
||||
newlines and \"double quotes\" and \'single quotes\'!
|
1
cmd/tofu/testdata/args-from-files/metachars-singlequote.txt
vendored
Normal file
1
cmd/tofu/testdata/args-from-files/metachars-singlequote.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
'in quotes >we take <redirection metacharacters | literally 2>&and as a normal; part of the overall string'
|
1
cmd/tofu/testdata/args-from-files/metachars.txt
vendored
Normal file
1
cmd/tofu/testdata/args-from-files/metachars.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
not a shell, so >we take <redirection metacharacters | literally 2>&but still accept them; blah
|
2
cmd/tofu/testdata/args-from-files/multiline.txt
vendored
Normal file
2
cmd/tofu/testdata/args-from-files/multiline.txt
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
bar baz
|
||||
beep boop
|
2
cmd/tofu/testdata/args-from-files/parens-quoted.txt
vendored
Normal file
2
cmd/tofu/testdata/args-from-files/parens-quoted.txt
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
"it's okay to have (parens) in double quotes"
|
||||
'and (also) in single quotes'
|
1
cmd/tofu/testdata/args-from-files/parens.txt
vendored
Normal file
1
cmd/tofu/testdata/args-from-files/parens.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
we do not allow (parentheses)
|
2
cmd/tofu/testdata/args-from-files/singlequote-multiline.txt
vendored
Normal file
2
cmd/tofu/testdata/args-from-files/singlequote-multiline.txt
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
'hello
|
||||
world'
|
1
cmd/tofu/testdata/args-from-files/singlequote-spaces.txt
vendored
Normal file
1
cmd/tofu/testdata/args-from-files/singlequote-spaces.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
' '
|
1
cmd/tofu/testdata/args-from-files/singlequote-unclosed.txt
vendored
Normal file
1
cmd/tofu/testdata/args-from-files/singlequote-unclosed.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
'uh-oh
|
1
cmd/tofu/testdata/args-from-files/singlequote.txt
vendored
Normal file
1
cmd/tofu/testdata/args-from-files/singlequote.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
-var='something=foo" "bar'
|
1
cmd/tofu/testdata/args-from-files/unix-style-path.txt
vendored
Normal file
1
cmd/tofu/testdata/args-from-files/unix-style-path.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
-var-file=../bar/baz.tfvars
|
1
cmd/tofu/testdata/args-from-files/unquoted.txt
vendored
Normal file
1
cmd/tofu/testdata/args-from-files/unquoted.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
-foo bar baz
|
3
cmd/tofu/testdata/args-from-files/windows-style-path.txt
vendored
Normal file
3
cmd/tofu/testdata/args-from-files/windows-style-path.txt
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
-var-file=..\bar\baz.tfvars
|
||||
-var-file='..\bar\baz.tfvars'
|
||||
-var-file=..\\bar\\baz.tfvars
|
Loading…
Reference in New Issue
Block a user