diff --git a/main.go b/main.go index 060bf12278b..572e08f85e7 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "math/rand" "os" "runtime" + "strings" "sync" "syscall" "time" @@ -123,6 +124,53 @@ func realMain() int { return 0 } +func extractChdirOption(args []string) (string, []string, error) { + if len(args) == 0 { + return "", args, nil + } + + const argName = "-chdir" + const argPrefix = argName + "=" + var argValue string + var argPos int + + for i, arg := range args { + if !strings.HasPrefix(arg, "-") { + // Because the chdir option is a subcommand-agnostic one, we require + // it to appear before any subcommand argument, so if we find a + // non-option before we find -chdir then we are finished. + break + } + if arg == argName || arg == argPrefix { + return "", args, fmt.Errorf("must include an equals sign followed by a directory path, like -chdir=example") + } + if strings.HasPrefix(arg, argPrefix) { + argPos = i + argValue = arg[len(argPrefix):] + break + } + } + + // When we fall out here, we'll have populated argValue with a non-empty + // string if the -chdir=... option was present and valid, or left it + // empty if it wasn't present. + if argValue == "" { + return "", args, nil + } + + // If we did find the option then we'll need to produce a new args that + // doesn't include it anymore. + if argPos == 0 { + // Easy case: we can just slice off the front + return argValue, args[1:], nil + } + // Otherwise we need to construct a new array and copy to it. + newArgs := make([]string, len(args)-1) + copy(newArgs, args[:argPos]) + copy(newArgs[argPos:], args[argPos+1:]) + return argValue, newArgs, nil +} + // wrappedMain is called only when we're wrapped by panicwrap and // returns the exit status to exit with. func wrappedMain() int { @@ -188,9 +236,27 @@ func wrappedMain() int { } log.Printf("[INFO] Setting cache directory: %s", cacheDir) + // Below is same as terraform's chdir setup + // https://github.com/hashicorp/terraform/blob/main/main.go + // The arguments can begin with a -chdir option to ask Terraform 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. + overrideWd, args, err := extractChdirOption(os.Args[1:]) + if err != nil { + fmt.Fprintf(os.Stdout, "%s Invalid -chdir option: \n\n%s\n", ErrorPrefix, err) + return 1 + } + if overrideWd != "" { + err := os.Chdir(overrideWd) + if err != nil { + fmt.Fprintf(os.Stdout, "%s Error handling -chdir option: \n\n%s\n", ErrorPrefix, err) + return 1 + } + } + // Determine if we're in machine-readable mode by mucking around with // the arguments... - args, machineReadable := extractMachineReadable(os.Args[1:]) + args, machineReadable := extractMachineReadable(args) defer packer.CleanupClients() diff --git a/main_test.go b/main_test.go index f4d44bbf120..bf18e07b004 100644 --- a/main_test.go +++ b/main_test.go @@ -33,6 +33,59 @@ func TestExcludeHelpFunc(t *testing.T) { } } +func TestExtractChdir(t *testing.T) { + cases := []struct { + desc, overrideWd string + args, expected []string + err error + }{ + { + desc: "TestHappyPath", + args: []string{"-chdir=example", "foo", "bar"}, + expected: []string{"foo", "bar"}, + overrideWd: "example", + err: nil, + }, + { + desc: "TestEmptyArgs", + args: []string{}, + expected: []string{}, + overrideWd: "", + err: nil, + }, + { + desc: "TestNoChdirArg", + args: []string{"foo", "bar"}, + expected: []string{"foo", "bar"}, + overrideWd: "", + err: nil, + }, + { + desc: "TestChdirNotFirst", + args: []string{"foo", "-chdir=example"}, + expected: []string{"foo", "-chdir=example"}, + overrideWd: "", + err: nil, + }, + } + + for _, tc := range cases { + overrideWd, args, err := extractChdirOption(tc.args) + if overrideWd != tc.overrideWd { + t.Fatalf("%s: bad overrideWd, expected: %s got: %s for args: %#v", + tc.desc, tc.overrideWd, overrideWd, tc.args) + } + + if !reflect.DeepEqual(args, tc.expected) { + t.Fatalf("%s: bad result args, expected: %#v, got: %#v for args: %#v", tc.desc, tc.expected, args, tc.args) + } + + if err != tc.err { + t.Fatalf("%s: bad err, expected: %s, got: %s for args: %#v", tc.desc, tc.err, err, tc.args) + } + } +} + func TestExtractMachineReadable(t *testing.T) { var args, expected, result []string var mr bool