From 0e1fcb1bf7ec0611b284c544adff4aa291886def Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Wed, 29 Jan 2025 15:23:43 -0800 Subject: [PATCH] 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 --- cmd/tofu/arguments.go | 128 ++++++ cmd/tofu/arguments_test.go | 404 ++++++++++++++++++ cmd/tofu/main.go | 62 +-- cmd/tofu/main_test.go | 235 ---------- .../testdata/args-from-files/backtick.txt | 1 + .../testdata/args-from-files/cmdsubst.txt | 1 + .../doublequote-envvar-like.txt | 1 + .../args-from-files/doublequote-multiline.txt | 2 + .../args-from-files/doublequote-spaces.txt | 1 + .../args-from-files/doublequote-unclosed.txt | 1 + .../testdata/args-from-files/doublequote.txt | 1 + cmd/tofu/testdata/args-from-files/empty.txt | 0 .../testdata/args-from-files/envvar-like.txt | 1 + cmd/tofu/testdata/args-from-files/escapes.txt | 2 + .../args-from-files/metachars-singlequote.txt | 1 + .../testdata/args-from-files/metachars.txt | 1 + .../testdata/args-from-files/multiline.txt | 2 + .../args-from-files/parens-quoted.txt | 2 + cmd/tofu/testdata/args-from-files/parens.txt | 1 + .../args-from-files/singlequote-multiline.txt | 2 + .../args-from-files/singlequote-spaces.txt | 1 + .../args-from-files/singlequote-unclosed.txt | 1 + .../testdata/args-from-files/singlequote.txt | 1 + .../args-from-files/unix-style-path.txt | 1 + .../testdata/args-from-files/unquoted.txt | 1 + .../args-from-files/windows-style-path.txt | 3 + 26 files changed, 574 insertions(+), 283 deletions(-) create mode 100644 cmd/tofu/arguments.go create mode 100644 cmd/tofu/arguments_test.go create mode 100644 cmd/tofu/testdata/args-from-files/backtick.txt create mode 100644 cmd/tofu/testdata/args-from-files/cmdsubst.txt create mode 100644 cmd/tofu/testdata/args-from-files/doublequote-envvar-like.txt create mode 100644 cmd/tofu/testdata/args-from-files/doublequote-multiline.txt create mode 100644 cmd/tofu/testdata/args-from-files/doublequote-spaces.txt create mode 100644 cmd/tofu/testdata/args-from-files/doublequote-unclosed.txt create mode 100644 cmd/tofu/testdata/args-from-files/doublequote.txt create mode 100644 cmd/tofu/testdata/args-from-files/empty.txt create mode 100644 cmd/tofu/testdata/args-from-files/envvar-like.txt create mode 100644 cmd/tofu/testdata/args-from-files/escapes.txt create mode 100644 cmd/tofu/testdata/args-from-files/metachars-singlequote.txt create mode 100644 cmd/tofu/testdata/args-from-files/metachars.txt create mode 100644 cmd/tofu/testdata/args-from-files/multiline.txt create mode 100644 cmd/tofu/testdata/args-from-files/parens-quoted.txt create mode 100644 cmd/tofu/testdata/args-from-files/parens.txt create mode 100644 cmd/tofu/testdata/args-from-files/singlequote-multiline.txt create mode 100644 cmd/tofu/testdata/args-from-files/singlequote-spaces.txt create mode 100644 cmd/tofu/testdata/args-from-files/singlequote-unclosed.txt create mode 100644 cmd/tofu/testdata/args-from-files/singlequote.txt create mode 100644 cmd/tofu/testdata/args-from-files/unix-style-path.txt create mode 100644 cmd/tofu/testdata/args-from-files/unquoted.txt create mode 100644 cmd/tofu/testdata/args-from-files/windows-style-path.txt diff --git a/cmd/tofu/arguments.go b/cmd/tofu/arguments.go new file mode 100644 index 0000000000..c6bbe44195 --- /dev/null +++ b/cmd/tofu/arguments.go @@ -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 +} diff --git a/cmd/tofu/arguments_test.go b/cmd/tofu/arguments_test.go new file mode 100644 index 0000000000..af34552cdd --- /dev/null +++ b/cmd/tofu/arguments_test.go @@ -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", "&but", "still", "accept", "them;", "blah"}, + }, + "metachars-singlequote.txt": { + WantExpanded: []string{"in quotes >we take &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) + } + }) + } +} diff --git a/cmd/tofu/main.go b/cmd/tofu/main.go index a95f71f920..4dffd70d30 100644 --- a/cmd/tofu/main.go +++ b/cmd/tofu/main.go @@ -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) { diff --git a/cmd/tofu/main_test.go b/cmd/tofu/main_test.go index ac2b990871..5ea3cfa900 100644 --- a/cmd/tofu/main_test.go +++ b/cmd/tofu/main_test.go @@ -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 diff --git a/cmd/tofu/testdata/args-from-files/backtick.txt b/cmd/tofu/testdata/args-from-files/backtick.txt new file mode 100644 index 0000000000..31336f5770 --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/backtick.txt @@ -0,0 +1 @@ +we don't expand `backtick sequences` because arbitrary command execution would be a significant security risk \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/cmdsubst.txt b/cmd/tofu/testdata/args-from-files/cmdsubst.txt new file mode 100644 index 0000000000..21ea3bb1e5 --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/cmdsubst.txt @@ -0,0 +1 @@ +we don't expand $(command substitutions) because arbitrary command execution would be a significant security risk \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/doublequote-envvar-like.txt b/cmd/tofu/testdata/args-from-files/doublequote-envvar-like.txt new file mode 100644 index 0000000000..c2b1398793 --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/doublequote-envvar-like.txt @@ -0,0 +1 @@ +"no $INTERPOLATION inside double quotes" \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/doublequote-multiline.txt b/cmd/tofu/testdata/args-from-files/doublequote-multiline.txt new file mode 100644 index 0000000000..2b202be9ca --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/doublequote-multiline.txt @@ -0,0 +1,2 @@ +"hello +world" \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/doublequote-spaces.txt b/cmd/tofu/testdata/args-from-files/doublequote-spaces.txt new file mode 100644 index 0000000000..4705c55a0b --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/doublequote-spaces.txt @@ -0,0 +1 @@ +" " \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/doublequote-unclosed.txt b/cmd/tofu/testdata/args-from-files/doublequote-unclosed.txt new file mode 100644 index 0000000000..c41582b3d6 --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/doublequote-unclosed.txt @@ -0,0 +1 @@ +"uh-oh \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/doublequote.txt b/cmd/tofu/testdata/args-from-files/doublequote.txt new file mode 100644 index 0000000000..c916c1c0a3 --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/doublequote.txt @@ -0,0 +1 @@ +"beep boop" foo "blah blah" diff --git a/cmd/tofu/testdata/args-from-files/empty.txt b/cmd/tofu/testdata/args-from-files/empty.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/tofu/testdata/args-from-files/envvar-like.txt b/cmd/tofu/testdata/args-from-files/envvar-like.txt new file mode 100644 index 0000000000..efec90273b --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/envvar-like.txt @@ -0,0 +1 @@ +we $DONT expand things that look like ${ENVVAR}interpolations \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/escapes.txt b/cmd/tofu/testdata/args-from-files/escapes.txt new file mode 100644 index 0000000000..bf849bec1a --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/escapes.txt @@ -0,0 +1,2 @@ +can escape\ spaces and\ +newlines and \"double quotes\" and \'single quotes\'! \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/metachars-singlequote.txt b/cmd/tofu/testdata/args-from-files/metachars-singlequote.txt new file mode 100644 index 0000000000..2f823836e9 --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/metachars-singlequote.txt @@ -0,0 +1 @@ +'in quotes >we take &and as a normal; part of the overall string' \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/metachars.txt b/cmd/tofu/testdata/args-from-files/metachars.txt new file mode 100644 index 0000000000..b289f7a9af --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/metachars.txt @@ -0,0 +1 @@ +not a shell, so >we take &but still accept them; blah \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/multiline.txt b/cmd/tofu/testdata/args-from-files/multiline.txt new file mode 100644 index 0000000000..54520e8f3a --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/multiline.txt @@ -0,0 +1,2 @@ +bar baz +beep boop diff --git a/cmd/tofu/testdata/args-from-files/parens-quoted.txt b/cmd/tofu/testdata/args-from-files/parens-quoted.txt new file mode 100644 index 0000000000..bb24a9cfc8 --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/parens-quoted.txt @@ -0,0 +1,2 @@ +"it's okay to have (parens) in double quotes" +'and (also) in single quotes' \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/parens.txt b/cmd/tofu/testdata/args-from-files/parens.txt new file mode 100644 index 0000000000..dabb6408cc --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/parens.txt @@ -0,0 +1 @@ +we do not allow (parentheses) \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/singlequote-multiline.txt b/cmd/tofu/testdata/args-from-files/singlequote-multiline.txt new file mode 100644 index 0000000000..1483b75480 --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/singlequote-multiline.txt @@ -0,0 +1,2 @@ +'hello +world' \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/singlequote-spaces.txt b/cmd/tofu/testdata/args-from-files/singlequote-spaces.txt new file mode 100644 index 0000000000..6a557eaa95 --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/singlequote-spaces.txt @@ -0,0 +1 @@ +' ' \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/singlequote-unclosed.txt b/cmd/tofu/testdata/args-from-files/singlequote-unclosed.txt new file mode 100644 index 0000000000..df2303a314 --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/singlequote-unclosed.txt @@ -0,0 +1 @@ +'uh-oh \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/singlequote.txt b/cmd/tofu/testdata/args-from-files/singlequote.txt new file mode 100644 index 0000000000..d299ab955a --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/singlequote.txt @@ -0,0 +1 @@ +-var='something=foo" "bar' \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/unix-style-path.txt b/cmd/tofu/testdata/args-from-files/unix-style-path.txt new file mode 100644 index 0000000000..4d8691e5b6 --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/unix-style-path.txt @@ -0,0 +1 @@ +-var-file=../bar/baz.tfvars \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/unquoted.txt b/cmd/tofu/testdata/args-from-files/unquoted.txt new file mode 100644 index 0000000000..e6a6e1793e --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/unquoted.txt @@ -0,0 +1 @@ +-foo bar baz \ No newline at end of file diff --git a/cmd/tofu/testdata/args-from-files/windows-style-path.txt b/cmd/tofu/testdata/args-from-files/windows-style-path.txt new file mode 100644 index 0000000000..ba0d3f2dd0 --- /dev/null +++ b/cmd/tofu/testdata/args-from-files/windows-style-path.txt @@ -0,0 +1,3 @@ +-var-file=..\bar\baz.tfvars +-var-file='..\bar\baz.tfvars' +-var-file=..\\bar\\baz.tfvars