1// Copyright 2018 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 mod edit 6 7package modcmd 8 9import ( 10 "bytes" 11 "context" 12 "encoding/json" 13 "errors" 14 "fmt" 15 "os" 16 "strings" 17 18 "cmd/go/internal/base" 19 "cmd/go/internal/gover" 20 "cmd/go/internal/lockedfile" 21 "cmd/go/internal/modfetch" 22 "cmd/go/internal/modload" 23 24 "golang.org/x/mod/modfile" 25 "golang.org/x/mod/module" 26) 27 28var cmdEdit = &base.Command{ 29 UsageLine: "go mod edit [editing flags] [-fmt|-print|-json] [go.mod]", 30 Short: "edit go.mod from tools or scripts", 31 Long: ` 32Edit provides a command-line interface for editing go.mod, 33for use primarily by tools or scripts. It reads only go.mod; 34it does not look up information about the modules involved. 35By default, edit reads and writes the go.mod file of the main module, 36but a different target file can be specified after the editing flags. 37 38The editing flags specify a sequence of editing operations. 39 40The -fmt flag reformats the go.mod file without making other changes. 41This reformatting is also implied by any other modifications that use or 42rewrite the go.mod file. The only time this flag is needed is if no other 43flags are specified, as in 'go mod edit -fmt'. 44 45The -module flag changes the module's path (the go.mod file's module line). 46 47The -godebug=key=value flag adds a godebug key=value line, 48replacing any existing godebug lines with the given key. 49 50The -dropgodebug=key flag drops any existing godebug lines 51with the given key. 52 53The -require=path@version and -droprequire=path flags 54add and drop a requirement on the given module path and version. 55Note that -require overrides any existing requirements on path. 56These flags are mainly for tools that understand the module graph. 57Users should prefer 'go get path@version' or 'go get path@none', 58which make other go.mod adjustments as needed to satisfy 59constraints imposed by other modules. 60 61The -go=version flag sets the expected Go language version. 62This flag is mainly for tools that understand Go version dependencies. 63Users should prefer 'go get go@version'. 64 65The -toolchain=version flag sets the Go toolchain to use. 66This flag is mainly for tools that understand Go version dependencies. 67Users should prefer 'go get toolchain@version'. 68 69The -exclude=path@version and -dropexclude=path@version flags 70add and drop an exclusion for the given module path and version. 71Note that -exclude=path@version is a no-op if that exclusion already exists. 72 73The -replace=old[@v]=new[@v] flag adds a replacement of the given 74module path and version pair. If the @v in old@v is omitted, a 75replacement without a version on the left side is added, which applies 76to all versions of the old module path. If the @v in new@v is omitted, 77the new path should be a local module root directory, not a module 78path. Note that -replace overrides any redundant replacements for old[@v], 79so omitting @v will drop existing replacements for specific versions. 80 81The -dropreplace=old[@v] flag drops a replacement of the given 82module path and version pair. If the @v is omitted, a replacement without 83a version on the left side is dropped. 84 85The -retract=version and -dropretract=version flags add and drop a 86retraction on the given version. The version may be a single version 87like "v1.2.3" or a closed interval like "[v1.1.0,v1.1.9]". Note that 88-retract=version is a no-op if that retraction already exists. 89 90The -godebug, -dropgodebug, -require, -droprequire, -exclude, -dropexclude, 91-replace, -dropreplace, -retract, and -dropretract editing flags may be 92repeated, and the changes are applied in the order given. 93 94The -print flag prints the final go.mod in its text format instead of 95writing it back to go.mod. 96 97The -json flag prints the final go.mod file in JSON format instead of 98writing it back to go.mod. The JSON output corresponds to these Go types: 99 100 type Module struct { 101 Path string 102 Version string 103 } 104 105 type GoMod struct { 106 Module ModPath 107 Go string 108 Toolchain string 109 Godebug []Godebug 110 Require []Require 111 Exclude []Module 112 Replace []Replace 113 Retract []Retract 114 } 115 116 type ModPath struct { 117 Path string 118 Deprecated string 119 } 120 121 type Godebug struct { 122 Key string 123 Value string 124 } 125 126 type Require struct { 127 Path string 128 Version string 129 Indirect bool 130 } 131 132 type Replace struct { 133 Old Module 134 New Module 135 } 136 137 type Retract struct { 138 Low string 139 High string 140 Rationale string 141 } 142 143Retract entries representing a single version (not an interval) will have 144the "Low" and "High" fields set to the same value. 145 146Note that this only describes the go.mod file itself, not other modules 147referred to indirectly. For the full set of modules available to a build, 148use 'go list -m -json all'. 149 150Edit also provides the -C, -n, and -x build flags. 151 152See https://golang.org/ref/mod#go-mod-edit for more about 'go mod edit'. 153 `, 154} 155 156var ( 157 editFmt = cmdEdit.Flag.Bool("fmt", false, "") 158 editGo = cmdEdit.Flag.String("go", "", "") 159 editToolchain = cmdEdit.Flag.String("toolchain", "", "") 160 editJSON = cmdEdit.Flag.Bool("json", false, "") 161 editPrint = cmdEdit.Flag.Bool("print", false, "") 162 editModule = cmdEdit.Flag.String("module", "", "") 163 edits []func(*modfile.File) // edits specified in flags 164) 165 166type flagFunc func(string) 167 168func (f flagFunc) String() string { return "" } 169func (f flagFunc) Set(s string) error { f(s); return nil } 170 171func init() { 172 cmdEdit.Run = runEdit // break init cycle 173 174 cmdEdit.Flag.Var(flagFunc(flagGodebug), "godebug", "") 175 cmdEdit.Flag.Var(flagFunc(flagDropGodebug), "dropgodebug", "") 176 cmdEdit.Flag.Var(flagFunc(flagRequire), "require", "") 177 cmdEdit.Flag.Var(flagFunc(flagDropRequire), "droprequire", "") 178 cmdEdit.Flag.Var(flagFunc(flagExclude), "exclude", "") 179 cmdEdit.Flag.Var(flagFunc(flagDropExclude), "dropexclude", "") 180 cmdEdit.Flag.Var(flagFunc(flagReplace), "replace", "") 181 cmdEdit.Flag.Var(flagFunc(flagDropReplace), "dropreplace", "") 182 cmdEdit.Flag.Var(flagFunc(flagRetract), "retract", "") 183 cmdEdit.Flag.Var(flagFunc(flagDropRetract), "dropretract", "") 184 185 base.AddBuildFlagsNX(&cmdEdit.Flag) 186 base.AddChdirFlag(&cmdEdit.Flag) 187 base.AddModCommonFlags(&cmdEdit.Flag) 188} 189 190func runEdit(ctx context.Context, cmd *base.Command, args []string) { 191 anyFlags := *editModule != "" || 192 *editGo != "" || 193 *editToolchain != "" || 194 *editJSON || 195 *editPrint || 196 *editFmt || 197 len(edits) > 0 198 199 if !anyFlags { 200 base.Fatalf("go: no flags specified (see 'go help mod edit').") 201 } 202 203 if *editJSON && *editPrint { 204 base.Fatalf("go: cannot use both -json and -print") 205 } 206 207 if len(args) > 1 { 208 base.Fatalf("go: too many arguments") 209 } 210 var gomod string 211 if len(args) == 1 { 212 gomod = args[0] 213 } else { 214 gomod = modload.ModFilePath() 215 } 216 217 if *editModule != "" { 218 if err := module.CheckImportPath(*editModule); err != nil { 219 base.Fatalf("go: invalid -module: %v", err) 220 } 221 } 222 223 if *editGo != "" && *editGo != "none" { 224 if !modfile.GoVersionRE.MatchString(*editGo) { 225 base.Fatalf(`go mod: invalid -go option; expecting something like "-go %s"`, gover.Local()) 226 } 227 } 228 if *editToolchain != "" && *editToolchain != "none" { 229 if !modfile.ToolchainRE.MatchString(*editToolchain) { 230 base.Fatalf(`go mod: invalid -toolchain option; expecting something like "-toolchain go%s"`, gover.Local()) 231 } 232 } 233 234 data, err := lockedfile.Read(gomod) 235 if err != nil { 236 base.Fatal(err) 237 } 238 239 modFile, err := modfile.Parse(gomod, data, nil) 240 if err != nil { 241 base.Fatalf("go: errors parsing %s:\n%s", base.ShortPath(gomod), err) 242 } 243 244 if *editModule != "" { 245 modFile.AddModuleStmt(*editModule) 246 } 247 248 if *editGo == "none" { 249 modFile.DropGoStmt() 250 } else if *editGo != "" { 251 if err := modFile.AddGoStmt(*editGo); err != nil { 252 base.Fatalf("go: internal error: %v", err) 253 } 254 } 255 if *editToolchain == "none" { 256 modFile.DropToolchainStmt() 257 } else if *editToolchain != "" { 258 if err := modFile.AddToolchainStmt(*editToolchain); err != nil { 259 base.Fatalf("go: internal error: %v", err) 260 } 261 } 262 263 if len(edits) > 0 { 264 for _, edit := range edits { 265 edit(modFile) 266 } 267 } 268 modFile.SortBlocks() 269 modFile.Cleanup() // clean file after edits 270 271 if *editJSON { 272 editPrintJSON(modFile) 273 return 274 } 275 276 out, err := modFile.Format() 277 if err != nil { 278 base.Fatal(err) 279 } 280 281 if *editPrint { 282 os.Stdout.Write(out) 283 return 284 } 285 286 // Make a best-effort attempt to acquire the side lock, only to exclude 287 // previous versions of the 'go' command from making simultaneous edits. 288 if unlock, err := modfetch.SideLock(ctx); err == nil { 289 defer unlock() 290 } 291 292 err = lockedfile.Transform(gomod, func(lockedData []byte) ([]byte, error) { 293 if !bytes.Equal(lockedData, data) { 294 return nil, errors.New("go.mod changed during editing; not overwriting") 295 } 296 return out, nil 297 }) 298 if err != nil { 299 base.Fatal(err) 300 } 301} 302 303// parsePathVersion parses -flag=arg expecting arg to be path@version. 304func parsePathVersion(flag, arg string) (path, version string) { 305 before, after, found := strings.Cut(arg, "@") 306 if !found { 307 base.Fatalf("go: -%s=%s: need path@version", flag, arg) 308 } 309 path, version = strings.TrimSpace(before), strings.TrimSpace(after) 310 if err := module.CheckImportPath(path); err != nil { 311 base.Fatalf("go: -%s=%s: invalid path: %v", flag, arg, err) 312 } 313 314 if !allowedVersionArg(version) { 315 base.Fatalf("go: -%s=%s: invalid version %q", flag, arg, version) 316 } 317 318 return path, version 319} 320 321// parsePath parses -flag=arg expecting arg to be path (not path@version). 322func parsePath(flag, arg string) (path string) { 323 if strings.Contains(arg, "@") { 324 base.Fatalf("go: -%s=%s: need just path, not path@version", flag, arg) 325 } 326 path = arg 327 if err := module.CheckImportPath(path); err != nil { 328 base.Fatalf("go: -%s=%s: invalid path: %v", flag, arg, err) 329 } 330 return path 331} 332 333// parsePathVersionOptional parses path[@version], using adj to 334// describe any errors. 335func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) { 336 if allowDirPath && modfile.IsDirectoryPath(arg) { 337 return arg, "", nil 338 } 339 before, after, found := strings.Cut(arg, "@") 340 if !found { 341 path = arg 342 } else { 343 path, version = strings.TrimSpace(before), strings.TrimSpace(after) 344 } 345 if err := module.CheckImportPath(path); err != nil { 346 return path, version, fmt.Errorf("invalid %s path: %v", adj, err) 347 } 348 if path != arg && !allowedVersionArg(version) { 349 return path, version, fmt.Errorf("invalid %s version: %q", adj, version) 350 } 351 return path, version, nil 352} 353 354// parseVersionInterval parses a single version like "v1.2.3" or a closed 355// interval like "[v1.2.3,v1.4.5]". Note that a single version has the same 356// representation as an interval with equal upper and lower bounds: both 357// Low and High are set. 358func parseVersionInterval(arg string) (modfile.VersionInterval, error) { 359 if !strings.HasPrefix(arg, "[") { 360 if !allowedVersionArg(arg) { 361 return modfile.VersionInterval{}, fmt.Errorf("invalid version: %q", arg) 362 } 363 return modfile.VersionInterval{Low: arg, High: arg}, nil 364 } 365 if !strings.HasSuffix(arg, "]") { 366 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg) 367 } 368 s := arg[1 : len(arg)-1] 369 before, after, found := strings.Cut(s, ",") 370 if !found { 371 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg) 372 } 373 low := strings.TrimSpace(before) 374 high := strings.TrimSpace(after) 375 if !allowedVersionArg(low) || !allowedVersionArg(high) { 376 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg) 377 } 378 return modfile.VersionInterval{Low: low, High: high}, nil 379} 380 381// allowedVersionArg returns whether a token may be used as a version in go.mod. 382// We don't call modfile.CheckPathVersion, because that insists on versions 383// being in semver form, but here we want to allow versions like "master" or 384// "1234abcdef", which the go command will resolve the next time it runs (or 385// during -fix). Even so, we need to make sure the version is a valid token. 386func allowedVersionArg(arg string) bool { 387 return !modfile.MustQuote(arg) 388} 389 390// flagGodebug implements the -godebug flag. 391func flagGodebug(arg string) { 392 key, value, ok := strings.Cut(arg, "=") 393 if !ok || strings.ContainsAny(arg, "\"`',") { 394 base.Fatalf("go: -godebug=%s: need key=value", arg) 395 } 396 edits = append(edits, func(f *modfile.File) { 397 if err := f.AddGodebug(key, value); err != nil { 398 base.Fatalf("go: -godebug=%s: %v", arg, err) 399 } 400 }) 401} 402 403// flagDropGodebug implements the -dropgodebug flag. 404func flagDropGodebug(arg string) { 405 edits = append(edits, func(f *modfile.File) { 406 if err := f.DropGodebug(arg); err != nil { 407 base.Fatalf("go: -dropgodebug=%s: %v", arg, err) 408 } 409 }) 410} 411 412// flagRequire implements the -require flag. 413func flagRequire(arg string) { 414 path, version := parsePathVersion("require", arg) 415 edits = append(edits, func(f *modfile.File) { 416 if err := f.AddRequire(path, version); err != nil { 417 base.Fatalf("go: -require=%s: %v", arg, err) 418 } 419 }) 420} 421 422// flagDropRequire implements the -droprequire flag. 423func flagDropRequire(arg string) { 424 path := parsePath("droprequire", arg) 425 edits = append(edits, func(f *modfile.File) { 426 if err := f.DropRequire(path); err != nil { 427 base.Fatalf("go: -droprequire=%s: %v", arg, err) 428 } 429 }) 430} 431 432// flagExclude implements the -exclude flag. 433func flagExclude(arg string) { 434 path, version := parsePathVersion("exclude", arg) 435 edits = append(edits, func(f *modfile.File) { 436 if err := f.AddExclude(path, version); err != nil { 437 base.Fatalf("go: -exclude=%s: %v", arg, err) 438 } 439 }) 440} 441 442// flagDropExclude implements the -dropexclude flag. 443func flagDropExclude(arg string) { 444 path, version := parsePathVersion("dropexclude", arg) 445 edits = append(edits, func(f *modfile.File) { 446 if err := f.DropExclude(path, version); err != nil { 447 base.Fatalf("go: -dropexclude=%s: %v", arg, err) 448 } 449 }) 450} 451 452// flagReplace implements the -replace flag. 453func flagReplace(arg string) { 454 before, after, found := strings.Cut(arg, "=") 455 if !found { 456 base.Fatalf("go: -replace=%s: need old[@v]=new[@w] (missing =)", arg) 457 } 458 old, new := strings.TrimSpace(before), strings.TrimSpace(after) 459 if strings.HasPrefix(new, ">") { 460 base.Fatalf("go: -replace=%s: separator between old and new is =, not =>", arg) 461 } 462 oldPath, oldVersion, err := parsePathVersionOptional("old", old, false) 463 if err != nil { 464 base.Fatalf("go: -replace=%s: %v", arg, err) 465 } 466 newPath, newVersion, err := parsePathVersionOptional("new", new, true) 467 if err != nil { 468 base.Fatalf("go: -replace=%s: %v", arg, err) 469 } 470 if newPath == new && !modfile.IsDirectoryPath(new) { 471 base.Fatalf("go: -replace=%s: unversioned new path must be local directory", arg) 472 } 473 474 edits = append(edits, func(f *modfile.File) { 475 if err := f.AddReplace(oldPath, oldVersion, newPath, newVersion); err != nil { 476 base.Fatalf("go: -replace=%s: %v", arg, err) 477 } 478 }) 479} 480 481// flagDropReplace implements the -dropreplace flag. 482func flagDropReplace(arg string) { 483 path, version, err := parsePathVersionOptional("old", arg, true) 484 if err != nil { 485 base.Fatalf("go: -dropreplace=%s: %v", arg, err) 486 } 487 edits = append(edits, func(f *modfile.File) { 488 if err := f.DropReplace(path, version); err != nil { 489 base.Fatalf("go: -dropreplace=%s: %v", arg, err) 490 } 491 }) 492} 493 494// flagRetract implements the -retract flag. 495func flagRetract(arg string) { 496 vi, err := parseVersionInterval(arg) 497 if err != nil { 498 base.Fatalf("go: -retract=%s: %v", arg, err) 499 } 500 edits = append(edits, func(f *modfile.File) { 501 if err := f.AddRetract(vi, ""); err != nil { 502 base.Fatalf("go: -retract=%s: %v", arg, err) 503 } 504 }) 505} 506 507// flagDropRetract implements the -dropretract flag. 508func flagDropRetract(arg string) { 509 vi, err := parseVersionInterval(arg) 510 if err != nil { 511 base.Fatalf("go: -dropretract=%s: %v", arg, err) 512 } 513 edits = append(edits, func(f *modfile.File) { 514 if err := f.DropRetract(vi); err != nil { 515 base.Fatalf("go: -dropretract=%s: %v", arg, err) 516 } 517 }) 518} 519 520// fileJSON is the -json output data structure. 521type fileJSON struct { 522 Module editModuleJSON 523 Go string `json:",omitempty"` 524 Toolchain string `json:",omitempty"` 525 Require []requireJSON 526 Exclude []module.Version 527 Replace []replaceJSON 528 Retract []retractJSON 529} 530 531type editModuleJSON struct { 532 Path string 533 Deprecated string `json:",omitempty"` 534} 535 536type requireJSON struct { 537 Path string 538 Version string `json:",omitempty"` 539 Indirect bool `json:",omitempty"` 540} 541 542type replaceJSON struct { 543 Old module.Version 544 New module.Version 545} 546 547type retractJSON struct { 548 Low string `json:",omitempty"` 549 High string `json:",omitempty"` 550 Rationale string `json:",omitempty"` 551} 552 553// editPrintJSON prints the -json output. 554func editPrintJSON(modFile *modfile.File) { 555 var f fileJSON 556 if modFile.Module != nil { 557 f.Module = editModuleJSON{ 558 Path: modFile.Module.Mod.Path, 559 Deprecated: modFile.Module.Deprecated, 560 } 561 } 562 if modFile.Go != nil { 563 f.Go = modFile.Go.Version 564 } 565 if modFile.Toolchain != nil { 566 f.Toolchain = modFile.Toolchain.Name 567 } 568 for _, r := range modFile.Require { 569 f.Require = append(f.Require, requireJSON{Path: r.Mod.Path, Version: r.Mod.Version, Indirect: r.Indirect}) 570 } 571 for _, x := range modFile.Exclude { 572 f.Exclude = append(f.Exclude, x.Mod) 573 } 574 for _, r := range modFile.Replace { 575 f.Replace = append(f.Replace, replaceJSON{r.Old, r.New}) 576 } 577 for _, r := range modFile.Retract { 578 f.Retract = append(f.Retract, retractJSON{r.Low, r.High, r.Rationale}) 579 } 580 data, err := json.MarshalIndent(&f, "", "\t") 581 if err != nil { 582 base.Fatalf("go: internal error: %v", err) 583 } 584 data = append(data, '\n') 585 os.Stdout.Write(data) 586} 587