1
0
mirror of https://github.com/golang/go synced 2024-09-28 17:14:29 -06:00

cmd/go: handle -C properly during toolchain switches

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>
This commit is contained in:
Russ Cox 2023-06-05 11:19:03 -04:00
parent e23273ddd4
commit 96d8d3eb32
8 changed files with 140 additions and 46 deletions

View File

@ -104,6 +104,7 @@
// 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
@ -1865,7 +1866,7 @@
//
// 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.
//
@ -1874,10 +1875,6 @@
// 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:
@ -1886,7 +1883,7 @@
// 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.

View 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 {

View 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")
}

View 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.

View 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

View 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)
}
}

View 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

View 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