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 := strings.Cut(arg, "@")
   282  	if !found {
   283  		path = arg
   284  	} else {
   285  		path, version = strings.TrimSpace(before), strings.TrimSpace(after)
   286  	}
   287  	if err := module.CheckImportPath(path); err != nil {
   288  		if !allowDirPath || !modfile.IsDirectoryPath(path) {
   289  			return path, version, fmt.Errorf("invalid %s path: %v", adj, err)
   290  		}
   291  	}
   292  	if path != arg && !allowedVersionArg(version) {
   293  		return path, version, fmt.Errorf("invalid %s version: %q", adj, version)
   294  	}
   295  	return path, version, nil
   296  }
   297  
   298  // flagEditworkReplace implements the -replace flag.
   299  func flagEditworkReplace(arg string) {
   300  	before, after, found := strings.Cut(arg, "=")
   301  	if !found {
   302  		base.Fatalf("go: -replace=%s: need old[@v]=new[@w] (missing =)", arg)
   303  	}
   304  	old, new := strings.TrimSpace(before), strings.TrimSpace(after)
   305  	if strings.HasPrefix(new, ">") {
   306  		base.Fatalf("go: -replace=%s: separator between old and new is =, not =>", arg)
   307  	}
   308  	oldPath, oldVersion, err := parsePathVersionOptional("old", old, false)
   309  	if err != nil {
   310  		base.Fatalf("go: -replace=%s: %v", arg, err)
   311  	}
   312  	newPath, newVersion, err := parsePathVersionOptional("new", new, true)
   313  	if err != nil {
   314  		base.Fatalf("go: -replace=%s: %v", arg, err)
   315  	}
   316  	if newPath == new && !modfile.IsDirectoryPath(new) {
   317  		base.Fatalf("go: -replace=%s: unversioned new path must be local directory", arg)
   318  	}
   319  
   320  	workedits = append(workedits, func(f *modfile.WorkFile) {
   321  		if err := f.AddReplace(oldPath, oldVersion, newPath, newVersion); err != nil {
   322  			base.Fatalf("go: -replace=%s: %v", arg, err)
   323  		}
   324  	})
   325  }
   326  
   327  // flagEditworkDropReplace implements the -dropreplace flag.
   328  func flagEditworkDropReplace(arg string) {
   329  	path, version, err := parsePathVersionOptional("old", arg, true)
   330  	if err != nil {
   331  		base.Fatalf("go: -dropreplace=%s: %v", arg, err)
   332  	}
   333  	workedits = append(workedits, func(f *modfile.WorkFile) {
   334  		if err := f.DropReplace(path, version); err != nil {
   335  			base.Fatalf("go: -dropreplace=%s: %v", arg, err)
   336  		}
   337  	})
   338  }
   339  
   340  type replaceJSON struct {
   341  	Old module.Version
   342  	New module.Version
   343  }
   344  
   345  // editPrintJSON prints the -json output.
   346  func editPrintJSON(workFile *modfile.WorkFile) {
   347  	var f workfileJSON
   348  	if workFile.Go != nil {
   349  		f.Go = workFile.Go.Version
   350  	}
   351  	for _, d := range workFile.Use {
   352  		f.Use = append(f.Use, useJSON{DiskPath: d.Path, ModPath: d.ModulePath})
   353  	}
   354  
   355  	for _, r := range workFile.Replace {
   356  		f.Replace = append(f.Replace, replaceJSON{r.Old, r.New})
   357  	}
   358  	data, err := json.MarshalIndent(&f, "", "\t")
   359  	if err != nil {
   360  		base.Fatalf("go: internal error: %v", err)
   361  	}
   362  	data = append(data, '\n')
   363  	os.Stdout.Write(data)
   364  }
   365  
   366  // workfileJSON is the -json output data structure.
   367  type workfileJSON struct {
   368  	Go      string `json:",omitempty"`
   369  	Use     []useJSON
   370  	Replace []replaceJSON
   371  }
   372  
   373  type useJSON struct {
   374  	DiskPath string
   375  	ModPath  string `json:",omitempty"`
   376  }
   377  

View as plain text