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  	if *editJSON && *editPrint {
   136  		base.Fatalf("go: cannot use both -json and -print")
   137  	}
   138  
   139  	if len(args) > 1 {
   140  		base.Fatalf("go: 'go help work edit' accepts at most one argument")
   141  	}
   142  	var gowork string
   143  	if len(args) == 1 {
   144  		gowork = args[0]
   145  	} else {
   146  		modload.InitWorkfile()
   147  		gowork = modload.WorkFilePath()
   148  	}
   149  	if gowork == "" {
   150  		base.Fatalf("go: no go.work file found\n\t(run 'go work init' first or specify path using GOWORK environment variable)")
   151  	}
   152  
   153  	if *editGo != "" && *editGo != "none" {
   154  		if !modfile.GoVersionRE.MatchString(*editGo) {
   155  			base.Fatalf(`go work: invalid -go option; expecting something like "-go %s"`, gover.Local())
   156  		}
   157  	}
   158  	if *editToolchain != "" && *editToolchain != "none" {
   159  		if !modfile.ToolchainRE.MatchString(*editToolchain) {
   160  			base.Fatalf(`go work: invalid -toolchain option; expecting something like "-toolchain go%s"`, gover.Local())
   161  		}
   162  	}
   163  
   164  	anyFlags := *editGo != "" ||
   165  		*editToolchain != "" ||
   166  		*editJSON ||
   167  		*editPrint ||
   168  		*editFmt ||
   169  		len(workedits) > 0
   170  
   171  	if !anyFlags {
   172  		base.Fatalf("go: no flags specified (see 'go help work edit').")
   173  	}
   174  
   175  	workFile, err := modload.ReadWorkFile(gowork)
   176  	if err != nil {
   177  		base.Fatalf("go: errors parsing %s:\n%s", base.ShortPath(gowork), err)
   178  	}
   179  
   180  	if *editGo == "none" {
   181  		workFile.DropGoStmt()
   182  	} else if *editGo != "" {
   183  		if err := workFile.AddGoStmt(*editGo); err != nil {
   184  			base.Fatalf("go: internal error: %v", err)
   185  		}
   186  	}
   187  	if *editToolchain == "none" {
   188  		workFile.DropToolchainStmt()
   189  	} else if *editToolchain != "" {
   190  		if err := workFile.AddToolchainStmt(*editToolchain); err != nil {
   191  			base.Fatalf("go: internal error: %v", err)
   192  		}
   193  	}
   194  
   195  	if len(workedits) > 0 {
   196  		for _, edit := range workedits {
   197  			edit(workFile)
   198  		}
   199  	}
   200  
   201  	workFile.SortBlocks()
   202  	workFile.Cleanup() // clean file after edits
   203  
   204  	// Note: No call to modload.UpdateWorkFile here.
   205  	// Edit's job is only to make the edits on the command line,
   206  	// not to apply the kinds of semantic changes that
   207  	// UpdateWorkFile does (or would eventually do, if we
   208  	// decide to add the module comments in go.work).
   209  
   210  	if *editJSON {
   211  		editPrintJSON(workFile)
   212  		return
   213  	}
   214  
   215  	if *editPrint {
   216  		os.Stdout.Write(modfile.Format(workFile.Syntax))
   217  		return
   218  	}
   219  
   220  	modload.WriteWorkFile(gowork, workFile)
   221  }
   222  
   223  // flagEditworkGodebug implements the -godebug flag.
   224  func flagEditworkGodebug(arg string) {
   225  	key, value, ok := strings.Cut(arg, "=")
   226  	if !ok || strings.ContainsAny(arg, "\"`',") {
   227  		base.Fatalf("go: -godebug=%s: need key=value", arg)
   228  	}
   229  	workedits = append(workedits, func(f *modfile.WorkFile) {
   230  		if err := f.AddGodebug(key, value); err != nil {
   231  			base.Fatalf("go: -godebug=%s: %v", arg, err)
   232  		}
   233  	})
   234  }
   235  
   236  // flagEditworkDropGodebug implements the -dropgodebug flag.
   237  func flagEditworkDropGodebug(arg string) {
   238  	workedits = append(workedits, func(f *modfile.WorkFile) {
   239  		if err := f.DropGodebug(arg); err != nil {
   240  			base.Fatalf("go: -dropgodebug=%s: %v", arg, err)
   241  		}
   242  	})
   243  }
   244  
   245  // flagEditworkUse implements the -use flag.
   246  func flagEditworkUse(arg string) {
   247  	workedits = append(workedits, func(f *modfile.WorkFile) {
   248  		_, mf, err := modload.ReadModFile(filepath.Join(arg, "go.mod"), nil)
   249  		modulePath := ""
   250  		if err == nil {
   251  			modulePath = mf.Module.Mod.Path
   252  		}
   253  		f.AddUse(modload.ToDirectoryPath(arg), modulePath)
   254  		if err := f.AddUse(modload.ToDirectoryPath(arg), ""); err != nil {
   255  			base.Fatalf("go: -use=%s: %v", arg, err)
   256  		}
   257  	})
   258  }
   259  
   260  // flagEditworkDropUse implements the -dropuse flag.
   261  func flagEditworkDropUse(arg string) {
   262  	workedits = append(workedits, func(f *modfile.WorkFile) {
   263  		if err := f.DropUse(modload.ToDirectoryPath(arg)); err != nil {
   264  			base.Fatalf("go: -dropdirectory=%s: %v", arg, err)
   265  		}
   266  	})
   267  }
   268  
   269  // allowedVersionArg returns whether a token may be used as a version in go.mod.
   270  // We don't call modfile.CheckPathVersion, because that insists on versions
   271  // being in semver form, but here we want to allow versions like "master" or
   272  // "1234abcdef", which the go command will resolve the next time it runs (or
   273  // during -fix).  Even so, we need to make sure the version is a valid token.
   274  func allowedVersionArg(arg string) bool {
   275  	return !modfile.MustQuote(arg)
   276  }
   277  
   278  // parsePathVersionOptional parses path[@version], using adj to
   279  // describe any errors.
   280  func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) {
   281  	before, after, found, err := modload.ParsePathVersion(arg)
   282  	if err != nil {
   283  		return "", "", err
   284  	}
   285  	if !found {
   286  		path = arg
   287  	} else {
   288  		path, version = strings.TrimSpace(before), strings.TrimSpace(after)
   289  	}
   290  	if err := module.CheckImportPath(path); err != nil {
   291  		if !allowDirPath || !modfile.IsDirectoryPath(path) {
   292  			return path, version, fmt.Errorf("invalid %s path: %v", adj, err)
   293  		}
   294  	}
   295  	if path != arg && !allowedVersionArg(version) {
   296  		return path, version, fmt.Errorf("invalid %s version: %q", adj, version)
   297  	}
   298  	return path, version, nil
   299  }
   300  
   301  // flagEditworkReplace implements the -replace flag.
   302  func flagEditworkReplace(arg string) {
   303  	before, after, found := strings.Cut(arg, "=")
   304  	if !found {
   305  		base.Fatalf("go: -replace=%s: need old[@v]=new[@w] (missing =)", arg)
   306  	}
   307  	old, new := strings.TrimSpace(before), strings.TrimSpace(after)
   308  	if strings.HasPrefix(new, ">") {
   309  		base.Fatalf("go: -replace=%s: separator between old and new is =, not =>", arg)
   310  	}
   311  	oldPath, oldVersion, err := parsePathVersionOptional("old", old, false)
   312  	if err != nil {
   313  		base.Fatalf("go: -replace=%s: %v", arg, err)
   314  	}
   315  	newPath, newVersion, err := parsePathVersionOptional("new", new, true)
   316  	if err != nil {
   317  		base.Fatalf("go: -replace=%s: %v", arg, err)
   318  	}
   319  	if newPath == new && !modfile.IsDirectoryPath(new) {
   320  		base.Fatalf("go: -replace=%s: unversioned new path must be local directory", arg)
   321  	}
   322  
   323  	workedits = append(workedits, func(f *modfile.WorkFile) {
   324  		if err := f.AddReplace(oldPath, oldVersion, newPath, newVersion); err != nil {
   325  			base.Fatalf("go: -replace=%s: %v", arg, err)
   326  		}
   327  	})
   328  }
   329  
   330  // flagEditworkDropReplace implements the -dropreplace flag.
   331  func flagEditworkDropReplace(arg string) {
   332  	path, version, err := parsePathVersionOptional("old", arg, true)
   333  	if err != nil {
   334  		base.Fatalf("go: -dropreplace=%s: %v", arg, err)
   335  	}
   336  	workedits = append(workedits, func(f *modfile.WorkFile) {
   337  		if err := f.DropReplace(path, version); err != nil {
   338  			base.Fatalf("go: -dropreplace=%s: %v", arg, err)
   339  		}
   340  	})
   341  }
   342  
   343  type replaceJSON struct {
   344  	Old module.Version
   345  	New module.Version
   346  }
   347  
   348  // editPrintJSON prints the -json output.
   349  func editPrintJSON(workFile *modfile.WorkFile) {
   350  	var f workfileJSON
   351  	if workFile.Go != nil {
   352  		f.Go = workFile.Go.Version
   353  	}
   354  	for _, d := range workFile.Use {
   355  		f.Use = append(f.Use, useJSON{DiskPath: d.Path, ModPath: d.ModulePath})
   356  	}
   357  
   358  	for _, r := range workFile.Replace {
   359  		f.Replace = append(f.Replace, replaceJSON{r.Old, r.New})
   360  	}
   361  	data, err := json.MarshalIndent(&f, "", "\t")
   362  	if err != nil {
   363  		base.Fatalf("go: internal error: %v", err)
   364  	}
   365  	data = append(data, '\n')
   366  	os.Stdout.Write(data)
   367  }
   368  
   369  // workfileJSON is the -json output data structure.
   370  type workfileJSON struct {
   371  	Go      string `json:",omitempty"`
   372  	Use     []useJSON
   373  	Replace []replaceJSON
   374  }
   375  
   376  type useJSON struct {
   377  	DiskPath string
   378  	ModPath  string `json:",omitempty"`
   379  }
   380  

View as plain text