From 7f67b321691a110678b00e632e2423998fe43db5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Feb 2017 14:05:37 -0800 Subject: [PATCH 1/6] main: add TF_CLI_ARGS to specify additional CLI args --- main.go | 47 ++++++++++++++++- main_test.go | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 main_test.go diff --git a/main.go b/main.go index bcf6a3f58..f0c736acc 100644 --- a/main.go +++ b/main.go @@ -13,11 +13,17 @@ import ( "github.com/hashicorp/terraform/helper/logging" "github.com/hashicorp/terraform/terraform" "github.com/mattn/go-colorable" + "github.com/mattn/go-shellwords" "github.com/mitchellh/cli" "github.com/mitchellh/panicwrap" "github.com/mitchellh/prefixedio" ) +const ( + // EnvCLI is the environment variable name to set additional CLI args. + EnvCLI = "TF_CLI_ARGS" +) + func main() { // Override global prefix set by go-dynect during init() log.SetPrefix("") @@ -129,9 +135,45 @@ func wrappedMain() int { // Make sure we clean up any managed plugins at the end of this defer plugin.CleanupClients() - // Get the command line args. We shortcut "--version" and "-v" to - // just show the version. + // Get the command line args. args := os.Args[1:] + + // Prefix the args with any args from the EnvCLI + if v := os.Getenv(EnvCLI); v != "" { + log.Printf("[INFO] %s value: %q", EnvCLI, v) + extra, err := shellwords.Parse(v) + if err != nil { + Ui.Error(fmt.Sprintf( + "Error parsing extra CLI args from %s: %s", + EnvCLI, err)) + return 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[0] != '-' { + 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:]) + args = newArgs + + } + + // We shortcut "--version" and "-v" to just show the version for _, arg := range args { if arg == "-v" || arg == "-version" || arg == "--version" { newArgs := make([]string, len(args)+1) @@ -142,6 +184,7 @@ func wrappedMain() int { } } + log.Printf("[INFO] CLI command args: %#v", args) cli := &cli.CLI{ Args: args, Commands: Commands, diff --git a/main_test.go b/main_test.go new file mode 100644 index 000000000..4c6cd5816 --- /dev/null +++ b/main_test.go @@ -0,0 +1,139 @@ +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, + }, + + { + // 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) + + // 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) + } + }) + } +} + +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 "" } From 488d5f41e6f328cc0f57f6d5da03e63032d54f44 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Feb 2017 14:10:01 -0800 Subject: [PATCH 2/6] vendor: add shellwords --- .../github.com/mattn/go-shellwords/README.md | 47 ++++++ .../mattn/go-shellwords/shellwords.go | 142 ++++++++++++++++++ .../mattn/go-shellwords/util_posix.go | 19 +++ .../mattn/go-shellwords/util_windows.go | 17 +++ vendor/vendor.json | 6 + 5 files changed, 231 insertions(+) create mode 100644 vendor/github.com/mattn/go-shellwords/README.md create mode 100644 vendor/github.com/mattn/go-shellwords/shellwords.go create mode 100644 vendor/github.com/mattn/go-shellwords/util_posix.go create mode 100644 vendor/github.com/mattn/go-shellwords/util_windows.go diff --git a/vendor/github.com/mattn/go-shellwords/README.md b/vendor/github.com/mattn/go-shellwords/README.md new file mode 100644 index 000000000..56f357fad --- /dev/null +++ b/vendor/github.com/mattn/go-shellwords/README.md @@ -0,0 +1,47 @@ +# go-shellwords + +[![Coverage Status](https://coveralls.io/repos/mattn/go-shellwords/badge.png?branch=master)](https://coveralls.io/r/mattn/go-shellwords?branch=master) +[![Build Status](https://travis-ci.org/mattn/go-shellwords.svg?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) diff --git a/vendor/github.com/mattn/go-shellwords/shellwords.go b/vendor/github.com/mattn/go-shellwords/shellwords.go new file mode 100644 index 000000000..355bdae68 --- /dev/null +++ b/vendor/github.com/mattn/go-shellwords/shellwords.go @@ -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) +} diff --git a/vendor/github.com/mattn/go-shellwords/util_posix.go b/vendor/github.com/mattn/go-shellwords/util_posix.go new file mode 100644 index 000000000..4f8ac55e4 --- /dev/null +++ b/vendor/github.com/mattn/go-shellwords/util_posix.go @@ -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 +} diff --git a/vendor/github.com/mattn/go-shellwords/util_windows.go b/vendor/github.com/mattn/go-shellwords/util_windows.go new file mode 100644 index 000000000..7cad4cf06 --- /dev/null +++ b/vendor/github.com/mattn/go-shellwords/util_windows.go @@ -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 +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 9546fb48e..87636bfd6 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -2107,6 +2107,12 @@ "revision": "30a891c33c7cde7b02a981314b4228ec99380cca", "revisionTime": "2016-11-23T14:36:37Z" }, + { + "checksumSHA1": "7OHq2KeND82oDITMEa+Mx4RmOaU=", + "path": "github.com/mattn/go-shellwords", + "revision": "753a2322a99f87c0eff284980e77f53041555bc6", + "revisionTime": "2017-01-23T01:43:24Z" + }, { "checksumSHA1": "A0PtnlAbuZA6kKfuVkM8GSx2SSI=", "comment": "v0.6.0", From f7e535ed6e702f0c26f4d0c242a1493f9997eb20 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Feb 2017 14:51:37 -0800 Subject: [PATCH 3/6] website: update website for TF_CLI_ARGS --- .../environment-variables.html.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/website/source/docs/configuration/environment-variables.html.md b/website/source/docs/configuration/environment-variables.html.md index 30e3b421a..e33954520 100644 --- a/website/source/docs/configuration/environment-variables.html.md +++ b/website/source/docs/configuration/environment-variables.html.md @@ -65,6 +65,29 @@ export TF_VAR_amap='{ foo = "bar", baz = "qux" }' For more on how to use `TF_VAR_name` in context, check out the section on [Variable Configuration](/docs/configuration/variables.html). +## TF_CLI_ARGS and TF_CLI_ARGS_name + +The value of `TF_CLI_ARGS` will specify additional arguments to the +command-line. This allows easier automation in CI environments as well as +modifying default behavior of Terraform on your own system. + +These arguments are inserted directly _after_ the subcommand +(such as `plan`) and _before_ any flags specified directly on the command-line. +This behavior ensures that flags on the command-line take precedence over +environment variables. + +For example, the following command: `TF_CLI_ARGS="-input=false" terraform apply -force` +is the equivalent to manually typing: `terraform apply -input=false -force`. + +The flag `TF_CLI_ARGS` affects all Terraform commands. If you specify a +named command in the form of `TF_CLI_ARGS_name` then it will only affect +that command. As an example, to specify that only plans never refresh, +you can set `TF_CLI_ARGS_plan="-refresh=false"`. + +The value of the flag is parsed as if you typed it directly to the shell. +Double and single quotes are allowed to capture strings and arguments will +be separated by spaces otherwise. + ## TF_SKIP_REMOTE_TESTS This can be set prior to running the unit tests to opt-out of any tests From 53796fcdb01e6350de843356032bf2c3e82823e7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Feb 2017 14:53:50 -0800 Subject: [PATCH 4/6] test for blank args for TF_CLI_ARGS --- main.go | 2 +- main_test.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index f0c736acc..6ab370753 100644 --- a/main.go +++ b/main.go @@ -153,7 +153,7 @@ func wrappedMain() int { // after the first non-flag arg. idx := -1 for i, v := range args { - if v[0] != '-' { + if len(v) > 0 && v[0] != '-' { idx = i break } diff --git a/main_test.go b/main_test.go index 4c6cd5816..154d27bf5 100644 --- a/main_test.go +++ b/main_test.go @@ -56,6 +56,22 @@ func TestMain_cliArgsFromEnv(t *testing.T) { 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 From df93e5120c7c4de420e629ffd04a867d4a5d9890 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Feb 2017 15:12:29 -0800 Subject: [PATCH 5/6] allow targeted TF_CLI_ARGS_x --- main.go | 93 +++++++++++++++++++++++++++++++++------------------- main_test.go | 82 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 33 deletions(-) diff --git a/main.go b/main.go index 6ab370753..7b88a5269 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "log" "os" "runtime" + "strings" "sync" "github.com/hashicorp/go-plugin" @@ -138,39 +139,27 @@ func wrappedMain() int { // Get the command line args. args := os.Args[1:] + // Build the CLI so far, we do this so we can query the subcommand. + cliRunner := &cli.CLI{ + Args: args, + Commands: Commands, + HelpFunc: helpFunc, + HelpWriter: os.Stdout, + } + // Prefix the args with any args from the EnvCLI - if v := os.Getenv(EnvCLI); v != "" { - log.Printf("[INFO] %s value: %q", EnvCLI, v) - extra, err := shellwords.Parse(v) - if err != nil { - Ui.Error(fmt.Sprintf( - "Error parsing extra CLI args from %s: %s", - EnvCLI, err)) - return 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 len(v) > 0 && v[0] != '-' { - 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:]) - args = newArgs + args, err = mergeEnvArgs(EnvCLI, args) + if err != nil { + Ui.Error(err.Error()) + return 1 + } + // Prefix the args with any args from the EnvCLI targeting this command + suffix := strings.Replace(cliRunner.Subcommand(), "-", "_", -1) + args, err = mergeEnvArgs(fmt.Sprintf("%s_%s", EnvCLI, suffix), args) + if err != nil { + Ui.Error(err.Error()) + return 1 } // We shortcut "--version" and "-v" to just show the version @@ -184,8 +173,9 @@ func wrappedMain() int { } } + // Rebuild the CLI with any modified args. log.Printf("[INFO] CLI command args: %#v", args) - cli := &cli.CLI{ + cliRunner = &cli.CLI{ Args: args, Commands: Commands, HelpFunc: helpFunc, @@ -196,7 +186,7 @@ func wrappedMain() int { ContextOpts.Providers = config.ProviderFactories() ContextOpts.Provisioners = config.ProvisionerFactories() - exitCode, err := cli.Run() + exitCode, err := cliRunner.Run() if err != nil { Ui.Error(fmt.Sprintf("Error executing CLI: %s", err.Error())) return 1 @@ -284,3 +274,40 @@ func copyOutput(r io.Reader, doneCh chan<- struct{}) { wg.Wait() } + +func mergeEnvArgs(envName 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 index to place the flags. We put them exactly + // after the first non-flag arg. + idx := -1 + for i, v := range args { + if len(v) > 0 && v[0] != '-' { + 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/main_test.go b/main_test.go index 154d27bf5..161c59715 100644 --- a/main_test.go +++ b/main_test.go @@ -110,6 +110,7 @@ func TestMain_cliArgsFromEnv(t *testing.T) { 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 != "" { @@ -142,6 +143,87 @@ func TestMain_cliArgsFromEnv(t *testing.T) { } } +// 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, + }, + } + + 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 } From 518ae5ef025ed93209a13aa55f3711025f31622a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Feb 2017 15:18:50 -0800 Subject: [PATCH 6/6] support nested subcommands with TF_CLI_ARGS --- main.go | 19 ++++++++++++++----- main_test.go | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 7b88a5269..e7750afa9 100644 --- a/main.go +++ b/main.go @@ -148,15 +148,17 @@ func wrappedMain() int { } // Prefix the args with any args from the EnvCLI - args, err = mergeEnvArgs(EnvCLI, args) + args, err = mergeEnvArgs(EnvCLI, cliRunner.Subcommand(), args) if err != nil { Ui.Error(err.Error()) return 1 } // Prefix the args with any args from the EnvCLI targeting this command - suffix := strings.Replace(cliRunner.Subcommand(), "-", "_", -1) - args, err = mergeEnvArgs(fmt.Sprintf("%s_%s", EnvCLI, suffix), args) + suffix := strings.Replace(strings.Replace( + cliRunner.Subcommand(), "-", "_", -1), " ", "_", -1) + args, err = mergeEnvArgs( + fmt.Sprintf("%s_%s", EnvCLI, suffix), cliRunner.Subcommand(), args) if err != nil { Ui.Error(err.Error()) return 1 @@ -275,7 +277,7 @@ func copyOutput(r io.Reader, doneCh chan<- struct{}) { wg.Wait() } -func mergeEnvArgs(envName string, args []string) ([]string, error) { +func mergeEnvArgs(envName string, cmd string, args []string) ([]string, error) { v := os.Getenv(envName) if v == "" { return args, nil @@ -289,11 +291,18 @@ func mergeEnvArgs(envName string, args []string) ([]string, error) { 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 len(v) > 0 && v[0] != '-' { + if v == search { idx = i break } diff --git a/main_test.go b/main_test.go index 161c59715..e5500e8ef 100644 --- a/main_test.go +++ b/main_test.go @@ -178,6 +178,26 @@ func TestMain_cliArgsFromEnvAdvanced(t *testing.T) { []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 {