mirror of https://github.com/hashicorp/terraform
Merge pull request #11922 from hashicorp/f-tf-cli
command: add TF_CLI_ARGS to specify additional CLI argspull/11929/head
commit
d2f9df37f5
@ -0,0 +1,257 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestMain_cliArgsFromEnv(t *testing.T) {
|
||||
// Setup 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 }()
|
||||
|
||||
// Setup test command and restore that
|
||||
testCommandName := "unit-test-cli-args"
|
||||
testCommand := &testCommandCLI{}
|
||||
defer func() { delete(Commands, testCommandName) }()
|
||||
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 bar",
|
||||
[]string{"-foo", "bar", "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) {
|
||||
os.Unsetenv(EnvCLI)
|
||||
defer os.Unsetenv(EnvCLI)
|
||||
|
||||
// Set the env var value
|
||||
if tc.Value != "" {
|
||||
if err := os.Setenv(EnvCLI, tc.Value); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup 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 := wrappedMain()
|
||||
if (exit != 0) != tc.Err {
|
||||
t.Fatalf("bad: %d", exit)
|
||||
}
|
||||
if tc.Err {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify
|
||||
if !reflect.DeepEqual(testCommand.Args, tc.Expected) {
|
||||
t.Fatalf("bad: %#v", 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 }()
|
||||
|
||||
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) {
|
||||
// Setup test command and restore that
|
||||
testCommandName := tc.Command
|
||||
testCommand := &testCommandCLI{}
|
||||
defer func() { delete(Commands, testCommandName) }()
|
||||
Commands[testCommandName] = func() (cli.Command, error) {
|
||||
return testCommand, nil
|
||||
}
|
||||
|
||||
os.Unsetenv(tc.EnvVar)
|
||||
defer os.Unsetenv(tc.EnvVar)
|
||||
|
||||
// Set the env var value
|
||||
if tc.Value != "" {
|
||||
if err := os.Setenv(tc.EnvVar, tc.Value); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup 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 := wrappedMain()
|
||||
if (exit != 0) != tc.Err {
|
||||
t.Fatalf("bad: %d", exit)
|
||||
}
|
||||
if tc.Err {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify
|
||||
if !reflect.DeepEqual(testCommand.Args, tc.Expected) {
|
||||
t.Fatalf("bad: %#v", testCommand.Args)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testCommandCLI struct {
|
||||
Args []string
|
||||
}
|
||||
|
||||
func (c *testCommandCLI) Run(args []string) int {
|
||||
c.Args = args
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *testCommandCLI) Synopsis() string { return "" }
|
||||
func (c *testCommandCLI) Help() string { return "" }
|
||||
@ -0,0 +1,47 @@
|
||||
# go-shellwords
|
||||
|
||||
[](https://coveralls.io/r/mattn/go-shellwords?branch=master)
|
||||
[](https://travis-ci.org/mattn/go-shellwords)
|
||||
|
||||
Parse line as shell words.
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
args, err := shellwords.Parse("./foo --bar=baz")
|
||||
// args should be ["./foo", "--bar=baz"]
|
||||
```
|
||||
|
||||
```go
|
||||
os.Setenv("FOO", "bar")
|
||||
p := shellwords.NewParser()
|
||||
p.ParseEnv = true
|
||||
args, err := p.Parse("./foo $FOO")
|
||||
// args should be ["./foo", "bar"]
|
||||
```
|
||||
|
||||
```go
|
||||
p := shellwords.NewParser()
|
||||
p.ParseBacktick = true
|
||||
args, err := p.Parse("./foo `echo $SHELL`")
|
||||
// args should be ["./foo", "/bin/bash"]
|
||||
```
|
||||
|
||||
```go
|
||||
shellwords.ParseBacktick = true
|
||||
p := shellwords.NewParser()
|
||||
args, err := p.Parse("./foo `echo $SHELL`")
|
||||
// args should be ["./foo", "/bin/bash"]
|
||||
```
|
||||
|
||||
# Thanks
|
||||
|
||||
This is based on cpan module [Parse::CommandLine](https://metacpan.org/pod/Parse::CommandLine).
|
||||
|
||||
# License
|
||||
|
||||
under the MIT License: http://mattn.mit-license.org/2014
|
||||
|
||||
# Author
|
||||
|
||||
Yasuhiro Matsumoto (a.k.a mattn)
|
||||
@ -0,0 +1,142 @@
|
||||
package shellwords
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
ParseEnv bool = false
|
||||
ParseBacktick bool = false
|
||||
)
|
||||
|
||||
var envRe = regexp.MustCompile(`\$({[a-zA-Z0-9_]+}|[a-zA-Z0-9_]+)`)
|
||||
|
||||
func isSpace(r rune) bool {
|
||||
switch r {
|
||||
case ' ', '\t', '\r', '\n':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func replaceEnv(s string) string {
|
||||
return envRe.ReplaceAllStringFunc(s, func(s string) string {
|
||||
s = s[1:]
|
||||
if s[0] == '{' {
|
||||
s = s[1 : len(s)-1]
|
||||
}
|
||||
return os.Getenv(s)
|
||||
})
|
||||
}
|
||||
|
||||
type Parser struct {
|
||||
ParseEnv bool
|
||||
ParseBacktick bool
|
||||
Position int
|
||||
}
|
||||
|
||||
func NewParser() *Parser {
|
||||
return &Parser{ParseEnv, ParseBacktick, 0}
|
||||
}
|
||||
|
||||
func (p *Parser) Parse(line string) ([]string, error) {
|
||||
args := []string{}
|
||||
buf := ""
|
||||
var escaped, doubleQuoted, singleQuoted, backQuote bool
|
||||
backtick := ""
|
||||
|
||||
pos := -1
|
||||
|
||||
loop:
|
||||
for i, r := range line {
|
||||
if escaped {
|
||||
buf += string(r)
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
if r == '\\' {
|
||||
if singleQuoted {
|
||||
buf += string(r)
|
||||
} else {
|
||||
escaped = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if isSpace(r) {
|
||||
if singleQuoted || doubleQuoted || backQuote {
|
||||
buf += string(r)
|
||||
backtick += string(r)
|
||||
} else if buf != "" {
|
||||
if p.ParseEnv {
|
||||
buf = replaceEnv(buf)
|
||||
}
|
||||
args = append(args, buf)
|
||||
buf = ""
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch r {
|
||||
case '`':
|
||||
if !singleQuoted && !doubleQuoted {
|
||||
if p.ParseBacktick {
|
||||
if backQuote {
|
||||
out, err := shellRun(backtick)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf = out
|
||||
}
|
||||
backtick = ""
|
||||
backQuote = !backQuote
|
||||
continue
|
||||
}
|
||||
backtick = ""
|
||||
backQuote = !backQuote
|
||||
}
|
||||
case '"':
|
||||
if !singleQuoted {
|
||||
doubleQuoted = !doubleQuoted
|
||||
continue
|
||||
}
|
||||
case '\'':
|
||||
if !doubleQuoted {
|
||||
singleQuoted = !singleQuoted
|
||||
continue
|
||||
}
|
||||
case ';', '&', '|', '<', '>':
|
||||
if !(escaped || singleQuoted || doubleQuoted || backQuote) {
|
||||
pos = i
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
buf += string(r)
|
||||
if backQuote {
|
||||
backtick += string(r)
|
||||
}
|
||||
}
|
||||
|
||||
if buf != "" {
|
||||
if p.ParseEnv {
|
||||
buf = replaceEnv(buf)
|
||||
}
|
||||
args = append(args, buf)
|
||||
}
|
||||
|
||||
if escaped || singleQuoted || doubleQuoted || backQuote {
|
||||
return nil, errors.New("invalid command line string")
|
||||
}
|
||||
|
||||
p.Position = pos
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func Parse(line string) ([]string, error) {
|
||||
return NewParser().Parse(line)
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
// +build !windows
|
||||
|
||||
package shellwords
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func shellRun(line string) (string, error) {
|
||||
shell := os.Getenv("SHELL")
|
||||
b, err := exec.Command(shell, "-c", line).Output()
|
||||
if err != nil {
|
||||
return "", errors.New(err.Error() + ":" + string(b))
|
||||
}
|
||||
return strings.TrimSpace(string(b)), nil
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package shellwords
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func shellRun(line string) (string, error) {
|
||||
shell := os.Getenv("COMSPEC")
|
||||
b, err := exec.Command(shell, "/c", line).Output()
|
||||
if err != nil {
|
||||
return "", errors.New(err.Error() + ":" + string(b))
|
||||
}
|
||||
return strings.TrimSpace(string(b)), nil
|
||||
}
|
||||
Loading…
Reference in new issue