mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
main: Try to expand arguments starting with "@" from files on disk
This is a GCC-like behavior where we preproces the argument vector by trying to treat any argument starting with "@" as a placeholder for arguments loaded from a given filename. As with GCC, if the remainder of the string cannot be used as a filename to open and read, the original argument is retained literally. However, if it _is_ possible to open and read a file of the given name then we take that as sufficient evidence of the operator's intent to read from that file and will fail with an error message if the file contents are not acceptable syntax. This uses the same upstream library as we've already been using for the TF_CLI_ARGS... environment variables. Since main.go was quite overcrowded with various different concerns, the handling of those environment variables also moves into the new arguments.go file here, since the two share an upstream dependency and both involve simplistic substitutions and insertions into the argument vector. The observable behavior of the environment variable handling should not be changed in any way by this commit. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
parent
6614782e6d
commit
0e1fcb1bf7
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