]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go: handle -C properly during toolchain switches
authorRuss Cox <rsc@golang.org>
Mon, 5 Jun 2023 15:19:03 +0000 (11:19 -0400)
committerRuss Cox <rsc@golang.org>
Tue, 6 Jun 2023 19:23:42 +0000 (19:23 +0000)
The -C dir flag was added in Go 1.20.
This CL adds a new restriction: the -C must appear as the first flag on the command line.
This restriction makes finding the -C flag robust and matches the general way
people tend to think about and use the -C flag anyway.
It may break a few scripts that have been written since Go 1.20
but hopefully they will not be hard to find and fix.
(There is no strict compatibility guarantee for the command line.)

For #57001.

Change-Id: Ice2e5982c58d41eabdaef42a80d3624cde2c9873
Reviewed-on: https://go-review.googlesource.com/c/go/+/500915
TryBot-Bypass: Russ Cox <rsc@golang.org>
Reviewed-by: Michael Matloob <matloob@golang.org>
src/cmd/go/alldocs.go
src/cmd/go/internal/base/base.go
src/cmd/go/internal/base/flag.go
src/cmd/go/internal/vet/vet.go
src/cmd/go/internal/work/build.go
src/cmd/go/main.go
src/cmd/go/testdata/script/chdir.txt
src/cmd/go/testdata/script/mod_get_exec_toolchain.txt

index 05ee094ea7499b1a65f403634f9203d5b1951fee..ccf5605a634ce61de3756f291b5d35a614b0c159 100644 (file)
 //             Change to dir before running the command.
 //             Any files named on the command line are interpreted after
 //             changing directories.
+//             If used, this flag must be the first one in the command line.
 //     -a
 //             force rebuilding of packages that are already up-to-date.
 //     -n
 //
 // Usage:
 //
-//     go vet [-C dir] [-n] [-x] [-vettool prog] [build flags] [vet flags] [packages]
+//     go vet [build flags] [-vettool prog] [vet flags] [packages]
 //
 // Vet runs the Go vet command on the packages named by the import paths.
 //
 // For a list of checkers and their flags, see 'go tool vet help'.
 // For details of a specific checker such as 'printf', see 'go tool vet help printf'.
 //
-// The -C flag changes to dir before running the 'go vet' command.
-// The -n flag prints commands that would be executed.
-// The -x flag prints commands as they are executed.
-//
 // The -vettool=prog flag selects a different analysis tool with alternative
 // or additional checks.
 // For example, the 'shadow' analyzer can be built and run using these commands:
 //     go vet -vettool=$(which shadow)
 //
 // The build flags supported by go vet are those that control package resolution
-// and execution, such as -n, -x, -v, -tags, and -toolexec.
+// and execution, such as -C, -n, -x, -v, -tags, and -toolexec.
 // For more about these flags, see 'go help build'.
 //
 // See also: go fmt, go fix.
index f3774ae2f0814e1bf7d5a5be3bbb429bc1536f5e..2171d139096a343c708450c613d80e923d8a7da4 100644 (file)
@@ -57,6 +57,20 @@ var Go = &Command{
        // Commands initialized in package main
 }
 
+// Lookup returns the subcommand with the given name, if any.
+// Otherwise it returns nil.
+//
+// Lookup ignores subcommands that have len(c.Commands) == 0 and c.Run == nil.
+// Such subcommands are only for use as arguments to "help".
+func (c *Command) Lookup(name string) *Command {
+       for _, sub := range c.Commands {
+               if sub.Name() == name && (len(c.Commands) > 0 || c.Runnable()) {
+                       return sub
+               }
+       }
+       return nil
+}
+
 // hasFlag reports whether a command or any of its subcommands contain the given
 // flag.
 func hasFlag(c *Command, name string) bool {
index 9d8d1c0c8db53b9a3eac82197e86e99cbec9f993..74e1275cfd077f59f19f2bb5e0813d6252dfe28a 100644 (file)
@@ -6,7 +6,7 @@ package base
 
 import (
        "flag"
-       "os"
+       "fmt"
 
        "cmd/go/internal/cfg"
        "cmd/go/internal/fsys"
@@ -62,7 +62,7 @@ func AddBuildFlagsNX(flags *flag.FlagSet) {
 func AddChdirFlag(flags *flag.FlagSet) {
        // The usage message is never printed, but it's used in chdir_test.go
        // to identify that the -C flag is from AddChdirFlag.
-       flags.Func("C", "AddChdirFlag", os.Chdir)
+       flags.Func("C", "AddChdirFlag", ChdirFlag)
 }
 
 // AddModFlag adds the -mod build flag to the flag set.
@@ -77,3 +77,9 @@ func AddModCommonFlags(flags *flag.FlagSet) {
        flags.StringVar(&cfg.ModFile, "modfile", "", "")
        flags.StringVar(&fsys.OverlayFile, "overlay", "", "")
 }
+
+func ChdirFlag(s string) error {
+       // main handles -C by removing it from the command line.
+       // If we see one during flag parsing, that's an error.
+       return fmt.Errorf("-C flag must be first flag on command line")
+}
index f59994c7c93a9e450e7772fd868b9f24179f9578..2d420971200bbcd4a5301bc985cb89c217a8be85 100644 (file)
@@ -25,7 +25,7 @@ func init() {
 
 var CmdVet = &base.Command{
        CustomFlags: true,
-       UsageLine:   "go vet [-C dir] [-n] [-x] [-vettool prog] [build flags] [vet flags] [packages]",
+       UsageLine:   "go vet [build flags] [-vettool prog] [vet flags] [packages]",
        Short:       "report likely mistakes in packages",
        Long: `
 Vet runs the Go vet command on the packages named by the import paths.
@@ -35,10 +35,6 @@ For more about specifying packages, see 'go help packages'.
 For a list of checkers and their flags, see 'go tool vet help'.
 For details of a specific checker such as 'printf', see 'go tool vet help printf'.
 
-The -C flag changes to dir before running the 'go vet' command.
-The -n flag prints commands that would be executed.
-The -x flag prints commands as they are executed.
-
 The -vettool=prog flag selects a different analysis tool with alternative
 or additional checks.
 For example, the 'shadow' analyzer can be built and run using these commands:
@@ -47,7 +43,7 @@ For example, the 'shadow' analyzer can be built and run using these commands:
   go vet -vettool=$(which shadow)
 
 The build flags supported by go vet are those that control package resolution
-and execution, such as -n, -x, -v, -tags, and -toolexec.
+and execution, such as -C, -n, -x, -v, -tags, and -toolexec.
 For more about these flags, see 'go help build'.
 
 See also: go fmt, go fix.
index e39e499930ce72f90368d05ae845d3e4ec793e9b..e2e0e07299694c633d18cd4569e6b381921917f5 100644 (file)
@@ -61,6 +61,7 @@ and test commands:
                Change to dir before running the command.
                Any files named on the command line are interpreted after
                changing directories.
+               If used, this flag must be the first one in the command line.
        -a
                force rebuilding of packages that are already up-to-date.
        -n
index d050792998a10314ed23f958f13478d94c99ed1f..00b0a2b78ba4a68ff762b1893c025b6675e65c2b 100644 (file)
@@ -18,6 +18,7 @@ import (
        "path/filepath"
        "runtime"
        rtrace "runtime/trace"
+       "slices"
        "strings"
 
        "cmd/go/internal/base"
@@ -92,6 +93,7 @@ var _ = go11tag
 
 func main() {
        log.SetFlags(0)
+       handleChdirFlag()
        toolchain.Select()
 
        flag.Usage = base.Usage
@@ -156,43 +158,61 @@ func main() {
                os.Exit(2)
        }
 
-BigCmdLoop:
-       for bigCmd := base.Go; ; {
-               for _, cmd := range bigCmd.Commands {
-                       if cmd.Name() != args[0] {
-                               continue
-                       }
-                       if len(cmd.Commands) > 0 {
-                               bigCmd = cmd
-                               args = args[1:]
-                               if len(args) == 0 {
-                                       help.PrintUsage(os.Stderr, bigCmd)
-                                       base.SetExitStatus(2)
-                                       base.Exit()
-                               }
-                               if args[0] == "help" {
-                                       // Accept 'go mod help' and 'go mod help foo' for 'go help mod' and 'go help mod foo'.
-                                       help.Help(os.Stdout, append(strings.Split(cfg.CmdName, " "), args[1:]...))
-                                       return
-                               }
-                               cfg.CmdName += " " + args[0]
-                               continue BigCmdLoop
-                       }
-                       if !cmd.Runnable() {
-                               continue
-                       }
-                       invoke(cmd, args)
+       cmd, used := lookupCmd(args)
+       cfg.CmdName = strings.Join(args[:used], " ")
+       if len(cmd.Commands) > 0 {
+               if used >= len(args) {
+                       help.PrintUsage(os.Stderr, cmd)
+                       base.SetExitStatus(2)
+                       base.Exit()
+               }
+               if args[used] == "help" {
+                       // Accept 'go mod help' and 'go mod help foo' for 'go help mod' and 'go help mod foo'.
+                       help.Help(os.Stdout, append(slices.Clip(args[:used]), args[used+1:]...))
                        base.Exit()
-                       return
                }
                helpArg := ""
-               if i := strings.LastIndex(cfg.CmdName, " "); i >= 0 {
-                       helpArg = " " + cfg.CmdName[:i]
+               if used > 0 {
+                       helpArg += " " + strings.Join(args[:used], " ")
                }
                fmt.Fprintf(os.Stderr, "go %s: unknown command\nRun 'go help%s' for usage.\n", cfg.CmdName, helpArg)
                base.SetExitStatus(2)
                base.Exit()
        }
+       invoke(cmd, args[used-1:])
+       base.Exit()
+}
+
+// lookupCmd interprets the initial elements of args
+// to find a command to run (cmd.Runnable() == true)
+// or else a command group that ran out of arguments
+// or had an unknown subcommand (len(cmd.Commands) > 0).
+// It returns that command and the number of elements of args
+// that it took to arrive at that command.
+func lookupCmd(args []string) (cmd *base.Command, used int) {
+       cmd = base.Go
+       for used < len(args) {
+               c := cmd.Lookup(args[used])
+               if c == nil {
+                       break
+               }
+               if c.Runnable() {
+                       cmd = c
+                       used++
+                       break
+               }
+               if len(c.Commands) > 0 {
+                       cmd = c
+                       used++
+                       if used >= len(args) || args[0] == "help" {
+                               break
+                       }
+                       continue
+               }
+               // len(c.Commands) == 0 && !c.Runnable() => help text; stop at "help"
+               break
+       }
+       return cmd, used
 }
 
 func invoke(cmd *base.Command, args []string) {
@@ -271,3 +291,44 @@ func maybeStartTrace(pctx context.Context) context.Context {
 
        return ctx
 }
+
+// handleChdirFlag handles the -C flag before doing anything else.
+// The -C flag must be the first flag on the command line, to make it easy to find
+// even with commands that have custom flag parsing.
+// handleChdirFlag handles the flag by chdir'ing to the directory
+// and then removing that flag from the command line entirely.
+//
+// We have to handle the -C flag this way for two reasons:
+//
+//   1. Toolchain selection needs to be in the right directory to look for go.mod and go.work.
+//
+//   2. A toolchain switch later on reinvokes the new go command with the same arguments.
+//      The parent toolchain has already done the chdir; the child must not try to do it again.
+func handleChdirFlag() {
+       _, used := lookupCmd(os.Args[1:])
+       used++ // because of [1:]
+       if used >= len(os.Args) {
+               return
+       }
+
+       var dir string
+       switch a := os.Args[used]; {
+       default:
+               return
+
+       case a == "-C", a == "--C":
+               if used+1 >= len(os.Args) {
+                       return
+               }
+               dir = os.Args[used+1]
+               os.Args = slices.Delete(os.Args, used, used+2)
+
+       case strings.HasPrefix(a, "-C="), strings.HasPrefix(a, "--C="):
+               _, dir, _ = strings.Cut(a, "=")
+               os.Args = slices.Delete(os.Args, used, used+1)
+       }
+
+       if err := os.Chdir(dir); err != nil {
+               base.Fatalf("go: %v", err)
+       }
+}
index 8952d18a72bbb9a7cbc78fbf5e79d134f9bf5042..a6feed6b45fce015b686a55e2bf24a0b5517633d 100644 (file)
@@ -17,15 +17,19 @@ go doc -C ../strings HasPrefix
 go env -C $OLD/custom GOMOD
 stdout 'custom[\\/]go.mod'
 ! go env -C ../nonexist
-stderr '^invalid value "../nonexist" for flag -C: chdir ../nonexist:.*$'
+stderr '^go: chdir ../nonexist: '
 
 # go test
-go test -n -C ../strings
+go test -C ../strings -n
 stderr 'strings\.test'
 
 # go vet
-go vet -n -C ../strings
+go vet -C ../strings -n
 stderr strings_test
 
+# -C must be first on command line (as of Go 1.21)
+! go test -n -C ../strings
+stderr '^invalid value "../strings" for flag -C: -C flag must be first flag on command line$'
+
 -- custom/go.mod --
 module m
index f78d517c87cd903db58f234eff6c2f32b30bb640..497fe36f4045b0c9cce3aaf20ef92575d8058b34 100644 (file)
@@ -122,6 +122,21 @@ stderr '^go: rsc.io/needgo124@v0.0.1 requires go >= 1.24; switching to go1.24rc1
 stderr '^go: upgraded go 1.1 => 1.24$'
 stderr '^go: added toolchain go1.24rc1$'
 
+# The -C flag should not happen more than once due to switching.
+mkdir dir dir/dir
+cp go.mod.new go.mod
+cp go.mod.new dir/go.mod
+cp go.mod.new dir/dir/go.mod
+cp p.go dir/p.go
+cp p.go dir/dir/p.go
+go get -C dir rsc.io/needgo124
+stderr '^go: rsc.io/needgo124@v0.0.1 requires go >= 1.24; switching to go1.24rc1$'
+stderr '^go: upgraded go 1.1 => 1.24$'
+stderr '^go: added toolchain go1.24rc1$'
+cmp go.mod.new go.mod
+cmp go.mod.new dir/dir/go.mod
+grep 'go 1.24$' dir/go.mod
+
 -- go.mod.new --
 module m
 go 1.1