Source file src/cmd/go/internal/workcmd/edit.go

     1  // Copyright 2021 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // go work edit
     6  
     7  package workcmd
     8  
     9  import (
    10  	"cmd/go/internal/base"
    11  	"cmd/go/internal/gover"
    12  	"cmd/go/internal/modload"
    13  	"context"
    14  	"encoding/json"
    15  	"fmt"
    16  	"os"
    17  	"path/filepath"
    18  	"strings"
    19  
    20  	"golang.org/x/mod/module"
    21  
    22  	"golang.org/x/mod/modfile"
    23  )
    24  
    25  var cmdEdit = &base.Command{
    26  	UsageLine: "go work edit [editing flags] [go.work]",
    27  	Short:     "edit go.work from tools or scripts",
    28  	Long: `Edit provides a command-line interface for editing go.work,
    29  for use primarily by tools or scripts. It only reads go.work;
    30  it does not look up information about the modules involved.
    31  If no file is specified, Edit looks for a go.work file in the current
    32  directory and its parent directories
    33  
    34  The editing flags specify a sequence of editing operations.
    35  
    36  The -fmt flag reformats the go.work file without making other changes.
    37  This reformatting is also implied by any other modifications that use or
    38  rewrite the go.mod file. The only time this flag is needed is if no other
    39  flags are specified, as in 'go work edit -fmt'.
    40  
    41  The -godebug=key=value flag adds a godebug key=value line,
    42  replacing any existing godebug lines with the given key.
    43  
    44  The -dropgodebug=key flag drops any existing godebug lines
    45  with the given key.
    46  
    47  The -use=path and -dropuse=path flags
    48  add and drop a use directive from the go.work file's set of module directories.
    49  
    50  The -replace=old[@v]=new[@v] flag adds a replacement of the given
    51  module path and version pair. If the @v in old@v is omitted, a
    52  replacement without a version on the left side is added, which applies
    53  to all versions of the old module path. If the @v in new@v is omitted,
    54  the new path should be a local module root directory, not a module
    55  path. Note that -replace overrides any redundant replacements for old[@v],
    56  so omitting @v will drop existing replacements for specific versions.
    57  
    58  The -dropreplace=old[@v] flag drops a replacement of the given
    59  module path and version pair. If the @v is omitted, a replacement without
    60  a version on the left side is dropped.
    61  
    62  The -use, -dropuse, -replace, and -dropreplace,
    63  editing flags may be repeated, and the changes are applied in the order given.
    64  
    65  The -go=version flag sets the expected Go language version.
    66  
    67  The -toolchain=name flag sets the Go toolchain to use.
    68  
    69  The -print flag prints the final go.work in its text format instead of
    70  writing it back to go.mod.
    71  
    72  The -json flag prints the final go.work file in JSON format instead of
    73  writing it back to go.mod. The JSON output corresponds to these Go types:
    74  
    75  	type GoWork struct {
    76  		Go        string
    77  		Toolchain string
    78  		Godebug   []Godebug
    79  		Use       []Use
    80  		Replace   []Replace
    81  	}
    82  
    83  	type Godebug struct {
    84  		Key   string
    85  		Value string
    86  	}
    87  
    88  	type Use struct {
    89  		DiskPath   string
    90  		ModulePath string
    91  	}
    92  
    93  	type Replace struct {
    94  		Old Module
    95  		New Module
    96  	}
    97  
    98  	type Module struct {
    99  		Path    string
   100  		Version string
   101  	}
   102  
   103  See the workspaces reference at https://go.dev/ref/mod#workspaces
   104  for more information.
   105  `,
   106  }
   107  
   108  var (
   109  	editFmt       = cmdEdit.Flag.Bool("fmt", false, "")
   110  	editGo        = cmdEdit.Flag.String("go", "", "")
   111  	editToolchain = cmdEdit.Flag.String("toolchain", "", "")
   112  	editJSON      = cmdEdit.Flag.Bool("json", false, "")
   113  	editPrint     = cmdEdit.Flag.Bool("print", false, "")
   114  	workedits     []func(file *modfile.WorkFile) // edits specified in flags
   115  )
   116  
   117  type flagFunc func(string)
   118  
   119  func (f flagFunc) String() string     { return "" }
   120  func (f flagFunc) Set(s string) error { f(s); return nil }
   121  
   122  func init() {
   123  	cmdEdit.Run = runEditwork // break init cycle
   124  
   125  	cmdEdit.Flag.Var(flagFunc(flagEditworkGodebug), "godebug", "")
   126  	cmdEdit.Flag.Var(flagFunc(flagEditworkDropGodebug), "dropgodebug", "")
   127  	cmdEdit.Flag.Var(flagFunc(flagEditworkUse), "use", "")
   128  	cmdEdit.Flag.Var(flagFunc(flagEditworkDropUse), "dropuse", "")
   129  	cmdEdit.Flag.Var(flagFunc(flagEditworkReplace), "replace", "")
   130  	cmdEdit.Flag.Var(flagFunc(flagEditworkDropReplace), "dropreplace", "")
   131  	base.AddChdirFlag(&cmdEdit.Flag)
   132  }
   133  
   134  func runEditwork(ctx context.Context, cmd *base.Command, args []string) {
   135  	moduleLoaderState := modload.NewState()
   136  	if *editJSON && *editPrint {
   137  		base.Fatalf("go: cannot use both -json and -print")
   138  	}
   139  
   140  	if len(args) > 1 {
   141  		base.Fatalf("go: 'go help work edit' accepts at most one argument")
   142  	}
   143  	var gowork string
   144  	if len(args) == 1 {
   145  		gowork = args[0]
   146  	} else {
   147  		moduleLoaderState.InitWorkfile()
   148  		gowork = modload.WorkFilePath(moduleLoaderState)
   149  	}
   150  	if gowork == "" {
   151  		base.Fatalf("go: no go.work file found\n\t(run 'go work init' first or specify path using GOWORK environment variable)")
   152  	}
   153  
   154  	if *editGo != "" && *editGo != "none" {
   155  		if !modfile.GoVersionRE.MatchString(*editGo) {
   156  			base.Fatalf(`go work: invalid -go option; expecting something like "-go %s"`, gover.Local())
   157  		}
   158  	}
   159  	if *editToolchain != "" && *editToolchain != "none" {
   160  		if !modfile.ToolchainRE.MatchString(*editToolchain) {
   161  			base.Fatalf(`go work: invalid -toolchain option; expecting something like "-toolchain go%s"`, gover.Local())
   162  		}
   163  	}
   164  
   165  	anyFlags := *editGo != "" ||
   166  		*editToolchain != "" ||
   167  		*editJSON ||
   168  		*editPrint ||
   169  		*editFmt ||
   170  		len(workedits) > 0
   171  
   172  	if !anyFlags {
   173  		base.Fatalf("go: no flags specified (see 'go help work edit').")
   174  	}
   175  
   176  	workFile, err := modload.ReadWorkFile(gowork)
   177  	if err != nil {
   178  		base.Fatalf("go: errors parsing %s:\n%s", base.ShortPath(gowork), err)
   179  	}
   180  
   181  	if *editGo == "none" {
   182  		workFile.DropGoStmt()
   183  	} else if *editGo != "" {
   184  		if err := workFile.AddGoStmt(*editGo); err != nil {
   185  			base.Fatalf("go: internal error: %v", err)
   186  		}
   187  	}
   188  	if *editToolchain == "none" {
   189  		workFile.DropToolchainStmt()
   190  	} else if *editToolchain != "" {
   191  		if err := workFile.AddToolchainStmt(*editToolchain); err != nil {
   192  			base.Fatalf("go: internal error: %v", err)
   193  		}
   194  	}
   195  
   196  	if len(workedits) > 0 {
   197  		for _, edit := range workedits {
   198  			edit(workFile)
   199  		}
   200  	}
   201  
   202  	workFile.SortBlocks()
   203  	workFile.Cleanup() // clean file after edits
   204  
   205  	// Note: No call to modload.UpdateWorkFile here.
   206  	// Edit's job is only to make the edits on the command line,
   207  	// not to apply the kinds of semantic changes that
   208  	// UpdateWorkFile does (or would eventually do, if we
   209  	// decide to add the module comments in go.work).
   210  
   211  	if *editJSON {
   212  		editPrintJSON(workFile)
   213  		return
   214  	}
   215  
   216  	if *editPrint {
   217  		os.Stdout.Write(modfile.Format(workFile.Syntax))
   218  		return
   219  	}
   220  
   221  	modload.WriteWorkFile(gowork, workFile)
   222  }
   223  
   224  // flagEditworkGodebug implements the -godebug flag.
   225  func flagEditworkGodebug(arg string) {
   226  	key, value, ok := strings.Cut(arg, "=")
   227  	if !ok || strings.ContainsAny(arg, "\"`',") {
   228  		base.Fatalf("go: -godebug=%s: need key=value", arg)
   229  	}
   230  	workedits = append(workedits, func(f *modfile.WorkFile) {
   231  		if err := f.AddGodebug(key, value); err != nil {
   232  			base.Fatalf("go: -godebug=%s: %v", arg, err)
   233  		}
   234  	})
   235  }
   236  
   237  // flagEditworkDropGodebug implements the -dropgodebug flag.
   238  func flagEditworkDropGodebug(arg string) {
   239  	workedits = append(workedits, func(f *modfile.WorkFile) {
   240  		if err := f.DropGodebug(arg); err != nil {
   241  			base.Fatalf("go: -dropgodebug=%s: %v", arg, err)
   242  		}
   243  	})
   244  }
   245  
   246  // flagEditworkUse implements the -use flag.
   247  func flagEditworkUse(arg string) {
   248  	workedits = append(workedits, func(f *modfile.WorkFile) {
   249  		_, mf, err := modload.ReadModFile(filepath.Join(arg, "go.mod"), nil)
   250  		modulePath := ""
   251  		if err == nil {
   252  			modulePath = mf.Module.Mod.Path
   253  		}
   254  		f.AddUse(modload.ToDirectoryPath(arg), modulePath)
   255  		if err := f.AddUse(modload.ToDirectoryPath(arg), ""); err != nil {
   256  			base.Fatalf("go: -use=%s: %v", arg, err)
   257  		}
   258  	})
   259  }
   260  
   261  // flagEditworkDropUse implements the -dropuse flag.
   262  func flagEditworkDropUse(arg string) {
   263  	workedits = append(workedits, func(f *modfile.WorkFile) {
   264  		if err := f.DropUse(modload.ToDirectoryPath(arg)); err != nil {
   265  			base.Fatalf("go: -dropdirectory=%s: %v", arg, err)
   266  		}
   267  	})
   268  }
   269  
   270  // allowedVersionArg returns whether a token may be used as a version in go.mod.
   271  // We don't call modfile.CheckPathVersion, because that insists on versions
   272  // being in semver form, but here we want to allow versions like "master" or
   273  // "1234abcdef", which the go command will resolve the next time it runs (or
   274  // during -fix).  Even so, we need to make sure the version is a valid token.
   275  func allowedVersionArg(arg string) bool {
   276  	return !modfile.MustQuote(arg)
   277  }
   278  
   279  // parsePathVersionOptional parses path[@version], using adj to
   280  // describe any errors.
   281  func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) {
   282  	before, after, found, err := modload.ParsePathVersion(arg)
   283  	if err != nil {
   284  		return "", "", err
   285  	}
   286  	if !found {
   287  		path = arg
   288  	} else {
   289  		path, version = strings.TrimSpace(before), strings.TrimSpace(after)
   290  	}
   291  	if err := module.CheckImportPath(path); err != nil {
   292  		if !allowDirPath || !modfile.IsDirectoryPath(path) {
   293  			return path, version, fmt.Errorf("invalid %s path: %v", adj, err)
   294  		}
   295  	}
   296  	if path != arg && !allowedVersionArg(version) {
   297  		return path, version, fmt.Errorf("invalid %s version: %q", adj, version)
   298  	}
   299  	return path, version, nil
   300  }
   301  
   302  // flagEditworkReplace implements the -replace flag.
   303  func flagEditworkReplace(arg string) {
   304  	before, after, found := strings.Cut(arg, "=")
   305  	if !found {
   306  		base.Fatalf("go: -replace=%s: need old[@v]=new[@w] (missing =)", arg)
   307  	}
   308  	old, new := strings.TrimSpace(before), strings.TrimSpace(after)
   309  	if strings.HasPrefix(new, ">") {
   310  		base.Fatalf("go: -replace=%s: separator between old and new is =, not =>", arg)
   311  	}
   312  	oldPath, oldVersion, err := parsePathVersionOptional("old", old, false)
   313  	if err != nil {
   314  		base.Fatalf("go: -replace=%s: %v", arg, err)
   315  	}
   316  	newPath, newVersion, err := parsePathVersionOptional("new", new, true)
   317  	if err != nil {
   318  		base.Fatalf("go: -replace=%s: %v", arg, err)
   319  	}
   320  	if newPath == new && !modfile.IsDirectoryPath(new) {
   321  		base.Fatalf("go: -replace=%s: unversioned new path must be local directory", arg)
   322  	}
   323  
   324  	workedits = append(workedits, func(f *modfile.WorkFile) {
   325  		if err := f.AddReplace(oldPath, oldVersion, newPath, newVersion); err != nil {
   326  			base.Fatalf("go: -replace=%s: %v", arg, err)
   327  		}
   328  	})
   329  }
   330  
   331  // flagEditworkDropReplace implements the -dropreplace flag.
   332  func flagEditworkDropReplace(arg string) {
   333  	path, version, err := parsePathVersionOptional("old", arg, true)
   334  	if err != nil {
   335  		base.Fatalf("go: -dropreplace=%s: %v", arg, err)
   336  	}
   337  	workedits = append(workedits, func(f *modfile.WorkFile) {
   338  		if err := f.DropReplace(path, version); err != nil {
   339  			base.Fatalf("go: -dropreplace=%s: %v", arg, err)
   340  		}
   341  	})
   342  }
   343  
   344  type replaceJSON struct {
   345  	Old module.Version
   346  	New module.Version
   347  }
   348  
   349  // editPrintJSON prints the -json output.
   350  func editPrintJSON(workFile *modfile.WorkFile) {
   351  	var f workfileJSON
   352  	if workFile.Go != nil {
   353  		f.Go = workFile.Go.Version
   354  	}
   355  	for _, d := range workFile.Use {
   356  		f.Use = append(f.Use, useJSON{DiskPath: d.Path, ModPath: d.ModulePath})
   357  	}
   358  
   359  	for _, r := range workFile.Replace {
   360  		f.Replace = append(f.Replace, replaceJSON{r.Old, r.New})
   361  	}
   362  	data, err := json.MarshalIndent(&f, "", "\t")
   363  	if err != nil {
   364  		base.Fatalf("go: internal error: %v", err)
   365  	}
   366  	data = append(data, '\n')
   367  	os.Stdout.Write(data)
   368  }
   369  
   370  // workfileJSON is the -json output data structure.
   371  type workfileJSON struct {
   372  	Go      string `json:",omitempty"`
   373  	Use     []useJSON
   374  	Replace []replaceJSON
   375  }
   376  
   377  type useJSON struct {
   378  	DiskPath string
   379  	ModPath  string `json:",omitempty"`
   380  }
   381  

View as plain text