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.work 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  It takes a version like "1.26" or "1.26.2".
    67  Using "none" as the version removes the go directive.
    68  
    69  The -toolchain=name flag sets the Go toolchain to use.
    70  It takes a toolchain name like "go1.26" or "go1.26.2".
    71  Using "none" as the name removes the toolchain directive.
    72  
    73  The -print flag prints the final go.work in its text format instead of
    74  writing it back to go.work.
    75  
    76  The -json flag prints the final go.work file in JSON format instead of
    77  writing it back to go.work. The JSON output corresponds to these Go types:
    78  
    79  	type GoWork struct {
    80  		Go        string
    81  		Toolchain string
    82  		Godebug   []Godebug
    83  		Use       []Use
    84  		Replace   []Replace
    85  	}
    86  
    87  	type Godebug struct {
    88  		Key   string
    89  		Value string
    90  	}
    91  
    92  	type Use struct {
    93  		DiskPath   string
    94  		ModulePath string
    95  	}
    96  
    97  	type Replace struct {
    98  		Old Module
    99  		New Module
   100  	}
   101  
   102  	type Module struct {
   103  		Path    string
   104  		Version string
   105  	}
   106  
   107  See the workspaces reference at https://go.dev/ref/mod#workspaces
   108  for more information.
   109  `,
   110  }
   111  
   112  var (
   113  	editFmt       = cmdEdit.Flag.Bool("fmt", false, "")
   114  	editGo        = cmdEdit.Flag.String("go", "", "")
   115  	editToolchain = cmdEdit.Flag.String("toolchain", "", "")
   116  	editJSON      = cmdEdit.Flag.Bool("json", false, "")
   117  	editPrint     = cmdEdit.Flag.Bool("print", false, "")
   118  	workedits     []func(file *modfile.WorkFile) // edits specified in flags
   119  )
   120  
   121  type flagFunc func(string)
   122  
   123  func (f flagFunc) String() string     { return "" }
   124  func (f flagFunc) Set(s string) error { f(s); return nil }
   125  
   126  func init() {
   127  	cmdEdit.Run = runEditwork // break init cycle
   128  
   129  	cmdEdit.Flag.Var(flagFunc(flagEditworkGodebug), "godebug", "")
   130  	cmdEdit.Flag.Var(flagFunc(flagEditworkDropGodebug), "dropgodebug", "")
   131  	cmdEdit.Flag.Var(flagFunc(flagEditworkUse), "use", "")
   132  	cmdEdit.Flag.Var(flagFunc(flagEditworkDropUse), "dropuse", "")
   133  	cmdEdit.Flag.Var(flagFunc(flagEditworkReplace), "replace", "")
   134  	cmdEdit.Flag.Var(flagFunc(flagEditworkDropReplace), "dropreplace", "")
   135  	base.AddChdirFlag(&cmdEdit.Flag)
   136  }
   137  
   138  func runEditwork(ctx context.Context, cmd *base.Command, args []string) {
   139  	moduleLoader := modload.NewLoader()
   140  	if *editJSON && *editPrint {
   141  		base.Fatalf("go: cannot use both -json and -print")
   142  	}
   143  
   144  	if len(args) > 1 {
   145  		base.Fatalf("go: 'go help work edit' accepts at most one argument")
   146  	}
   147  	var gowork string
   148  	if len(args) == 1 {
   149  		gowork = args[0]
   150  	} else {
   151  		moduleLoader.InitWorkfile()
   152  		gowork = modload.WorkFilePath(moduleLoader)
   153  	}
   154  	if gowork == "" {
   155  		base.Fatalf("go: no go.work file found\n\t(run 'go work init' first or specify path using GOWORK environment variable)")
   156  	}
   157  
   158  	if *editGo != "" && *editGo != "none" {
   159  		if !modfile.GoVersionRE.MatchString(*editGo) {
   160  			base.Fatalf(`go work: invalid -go option; expecting something like "-go %s"`, gover.Local())
   161  		}
   162  	}
   163  	if *editToolchain != "" && *editToolchain != "none" {
   164  		if !modfile.ToolchainRE.MatchString(*editToolchain) {
   165  			base.Fatalf(`go work: invalid -toolchain option; expecting something like "-toolchain go%s"`, gover.Local())
   166  		}
   167  	}
   168  
   169  	anyFlags := *editGo != "" ||
   170  		*editToolchain != "" ||
   171  		*editJSON ||
   172  		*editPrint ||
   173  		*editFmt ||
   174  		len(workedits) > 0
   175  
   176  	if !anyFlags {
   177  		base.Fatalf("go: no flags specified (see 'go help work edit').")
   178  	}
   179  
   180  	workFile, err := modload.ReadWorkFile(gowork)
   181  	if err != nil {
   182  		base.Fatalf("go: errors parsing %s:\n%s", base.ShortPath(gowork), err)
   183  	}
   184  
   185  	if *editGo == "none" {
   186  		workFile.DropGoStmt()
   187  	} else if *editGo != "" {
   188  		if err := workFile.AddGoStmt(*editGo); err != nil {
   189  			base.Fatalf("go: internal error: %v", err)
   190  		}
   191  	}
   192  	if *editToolchain == "none" {
   193  		workFile.DropToolchainStmt()
   194  	} else if *editToolchain != "" {
   195  		if err := workFile.AddToolchainStmt(*editToolchain); err != nil {
   196  			base.Fatalf("go: internal error: %v", err)
   197  		}
   198  	}
   199  
   200  	if len(workedits) > 0 {
   201  		for _, edit := range workedits {
   202  			edit(workFile)
   203  		}
   204  	}
   205  
   206  	workFile.SortBlocks()
   207  	workFile.Cleanup() // clean file after edits
   208  
   209  	// Note: No call to modload.UpdateWorkFile here.
   210  	// Edit's job is only to make the edits on the command line,
   211  	// not to apply the kinds of semantic changes that
   212  	// UpdateWorkFile does (or would eventually do, if we
   213  	// decide to add the module comments in go.work).
   214  
   215  	if *editJSON {
   216  		editPrintJSON(workFile)
   217  		return
   218  	}
   219  
   220  	if *editPrint {
   221  		os.Stdout.Write(modfile.Format(workFile.Syntax))
   222  		return
   223  	}
   224  
   225  	modload.WriteWorkFile(gowork, workFile)
   226  }
   227  
   228  // flagEditworkGodebug implements the -godebug flag.
   229  func flagEditworkGodebug(arg string) {
   230  	key, value, ok := strings.Cut(arg, "=")
   231  	if !ok || strings.ContainsAny(arg, "\"`',") {
   232  		base.Fatalf("go: -godebug=%s: need key=value", arg)
   233  	}
   234  	workedits = append(workedits, func(f *modfile.WorkFile) {
   235  		if err := f.AddGodebug(key, value); err != nil {
   236  			base.Fatalf("go: -godebug=%s: %v", arg, err)
   237  		}
   238  	})
   239  }
   240  
   241  // flagEditworkDropGodebug implements the -dropgodebug flag.
   242  func flagEditworkDropGodebug(arg string) {
   243  	workedits = append(workedits, func(f *modfile.WorkFile) {
   244  		if err := f.DropGodebug(arg); err != nil {
   245  			base.Fatalf("go: -dropgodebug=%s: %v", arg, err)
   246  		}
   247  	})
   248  }
   249  
   250  // flagEditworkUse implements the -use flag.
   251  func flagEditworkUse(arg string) {
   252  	workedits = append(workedits, func(f *modfile.WorkFile) {
   253  		_, mf, err := modload.ReadModFile(filepath.Join(arg, "go.mod"), nil)
   254  		modulePath := ""
   255  		if err == nil {
   256  			modulePath = mf.Module.Mod.Path
   257  		}
   258  		f.AddUse(modload.ToDirectoryPath(arg), modulePath)
   259  		if err := f.AddUse(modload.ToDirectoryPath(arg), ""); err != nil {
   260  			base.Fatalf("go: -use=%s: %v", arg, err)
   261  		}
   262  	})
   263  }
   264  
   265  // flagEditworkDropUse implements the -dropuse flag.
   266  func flagEditworkDropUse(arg string) {
   267  	workedits = append(workedits, func(f *modfile.WorkFile) {
   268  		if err := f.DropUse(modload.ToDirectoryPath(arg)); err != nil {
   269  			base.Fatalf("go: -dropuse=%s: %v", arg, err)
   270  		}
   271  	})
   272  }
   273  
   274  // allowedVersionArg returns whether a token may be used as a version in go.mod.
   275  // We don't call modfile.CheckPathVersion, because that insists on versions
   276  // being in semver form, but here we want to allow versions like "master" or
   277  // "1234abcdef", which the go command will resolve the next time it runs (or
   278  // during -fix).  Even so, we need to make sure the version is a valid token.
   279  func allowedVersionArg(arg string) bool {
   280  	return !modfile.MustQuote(arg)
   281  }
   282  
   283  // parsePathVersionOptional parses path[@version], using adj to
   284  // describe any errors.
   285  func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) {
   286  	before, after, found, err := modload.ParsePathVersion(arg)
   287  	if err != nil {
   288  		return "", "", err
   289  	}
   290  	if !found {
   291  		path = arg
   292  	} else {
   293  		path, version = strings.TrimSpace(before), strings.TrimSpace(after)
   294  	}
   295  	if err := module.CheckImportPath(path); err != nil {
   296  		if !allowDirPath || !modfile.IsDirectoryPath(path) {
   297  			return path, version, fmt.Errorf("invalid %s path: %v", adj, err)
   298  		}
   299  	}
   300  	if path != arg && !allowedVersionArg(version) {
   301  		return path, version, fmt.Errorf("invalid %s version: %q", adj, version)
   302  	}
   303  	return path, version, nil
   304  }
   305  
   306  // flagEditworkReplace implements the -replace flag.
   307  func flagEditworkReplace(arg string) {
   308  	before, after, found := strings.Cut(arg, "=")
   309  	if !found {
   310  		base.Fatalf("go: -replace=%s: need old[@v]=new[@w] (missing =)", arg)
   311  	}
   312  	old, new := strings.TrimSpace(before), strings.TrimSpace(after)
   313  	if strings.HasPrefix(new, ">") {
   314  		base.Fatalf("go: -replace=%s: separator between old and new is =, not =>", arg)
   315  	}
   316  	oldPath, oldVersion, err := parsePathVersionOptional("old", old, false)
   317  	if err != nil {
   318  		base.Fatalf("go: -replace=%s: %v", arg, err)
   319  	}
   320  	newPath, newVersion, err := parsePathVersionOptional("new", new, true)
   321  	if err != nil {
   322  		base.Fatalf("go: -replace=%s: %v", arg, err)
   323  	}
   324  	if newPath == new && !modfile.IsDirectoryPath(new) {
   325  		base.Fatalf("go: -replace=%s: unversioned new path must be local directory", arg)
   326  	}
   327  
   328  	workedits = append(workedits, func(f *modfile.WorkFile) {
   329  		if err := f.AddReplace(oldPath, oldVersion, newPath, newVersion); err != nil {
   330  			base.Fatalf("go: -replace=%s: %v", arg, err)
   331  		}
   332  	})
   333  }
   334  
   335  // flagEditworkDropReplace implements the -dropreplace flag.
   336  func flagEditworkDropReplace(arg string) {
   337  	path, version, err := parsePathVersionOptional("old", arg, true)
   338  	if err != nil {
   339  		base.Fatalf("go: -dropreplace=%s: %v", arg, err)
   340  	}
   341  	workedits = append(workedits, func(f *modfile.WorkFile) {
   342  		if err := f.DropReplace(path, version); err != nil {
   343  			base.Fatalf("go: -dropreplace=%s: %v", arg, err)
   344  		}
   345  	})
   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  	if workFile.Toolchain != nil {
   355  		f.Toolchain = workFile.Toolchain.Name
   356  	}
   357  	for _, d := range workFile.Godebug {
   358  		f.Godebug = append(f.Godebug, godebugJSON{Key: d.Key, Value: d.Value})
   359  	}
   360  	for _, d := range workFile.Use {
   361  		f.Use = append(f.Use, useJSON{DiskPath: d.Path, ModPath: d.ModulePath})
   362  	}
   363  	for _, r := range workFile.Replace {
   364  		f.Replace = append(f.Replace, replaceJSON{r.Old, r.New})
   365  	}
   366  	data, err := json.MarshalIndent(&f, "", "\t")
   367  	if err != nil {
   368  		base.Fatalf("go: internal error: %v", err)
   369  	}
   370  	data = append(data, '\n')
   371  	os.Stdout.Write(data)
   372  }
   373  
   374  // workfileJSON is the -json output data structure.
   375  type workfileJSON struct {
   376  	Go        string        `json:",omitempty"`
   377  	Toolchain string        `json:",omitempty"`
   378  	Godebug   []godebugJSON `json:",omitempty"`
   379  	Use       []useJSON     `json:",omitempty"`
   380  	Replace   []replaceJSON `json:",omitempty"`
   381  }
   382  
   383  type godebugJSON struct {
   384  	Key   string
   385  	Value string
   386  }
   387  
   388  type useJSON struct {
   389  	DiskPath string
   390  	ModPath  string `json:",omitempty"`
   391  }
   392  
   393  type replaceJSON struct {
   394  	Old module.Version
   395  	New module.Version
   396  }
   397  

View as plain text