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.
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"
24 "golang.org/x/mod/modfile"
25 "golang.org/x/mod/module"
28 var cmdEdit = &base.Command{
29 UsageLine: "go mod edit [editing flags] [-fmt|-print|-json] [go.mod]",
30 Short: "edit go.mod from tools or scripts",
32 Edit provides a command-line interface for editing go.mod,
33 for use primarily by tools or scripts. It reads only go.mod;
34 it does not look up information about the modules involved.
35 By default, edit reads and writes the go.mod file of the main module,
36 but a different target file can be specified after the editing flags.
38 The editing flags specify a sequence of editing operations.
40 The -fmt flag reformats the go.mod file without making other changes.
41 This reformatting is also implied by any other modifications that use or
42 rewrite the go.mod file. The only time this flag is needed is if no other
43 flags are specified, as in 'go mod edit -fmt'.
45 The -module flag changes the module's path (the go.mod file's module line).
47 The -require=path@version and -droprequire=path flags
48 add and drop a requirement on the given module path and version.
49 Note that -require overrides any existing requirements on path.
50 These flags are mainly for tools that understand the module graph.
51 Users should prefer 'go get path@version' or 'go get path@none',
52 which make other go.mod adjustments as needed to satisfy
53 constraints imposed by other modules.
55 The -exclude=path@version and -dropexclude=path@version flags
56 add and drop an exclusion for the given module path and version.
57 Note that -exclude=path@version is a no-op if that exclusion already exists.
59 The -replace=old[@v]=new[@v] flag adds a replacement of the given
60 module path and version pair. If the @v in old@v is omitted, a
61 replacement without a version on the left side is added, which applies
62 to all versions of the old module path. If the @v in new@v is omitted,
63 the new path should be a local module root directory, not a module
64 path. Note that -replace overrides any redundant replacements for old[@v],
65 so omitting @v will drop existing replacements for specific versions.
67 The -dropreplace=old[@v] flag drops a replacement of the given
68 module path and version pair. If the @v is omitted, a replacement without
69 a version on the left side is dropped.
71 The -retract=version and -dropretract=version flags add and drop a
72 retraction on the given version. The version may be a single version
73 like "v1.2.3" or a closed interval like "[v1.1.0,v1.1.9]". Note that
74 -retract=version is a no-op if that retraction already exists.
76 The -require, -droprequire, -exclude, -dropexclude, -replace,
77 -dropreplace, -retract, and -dropretract editing flags may be repeated,
78 and the changes are applied in the order given.
80 The -go=version flag sets the expected Go language version.
82 The -toolchain=name flag sets the Go toolchain to use.
84 The -print flag prints the final go.mod in its text format instead of
85 writing it back to go.mod.
87 The -json flag prints the final go.mod file in JSON format instead of
88 writing it back to go.mod. The JSON output corresponds to these Go types:
105 type ModPath struct {
110 type Require struct {
116 type Replace struct {
121 type Retract struct {
127 Retract entries representing a single version (not an interval) will have
128 the "Low" and "High" fields set to the same value.
130 Note that this only describes the go.mod file itself, not other modules
131 referred to indirectly. For the full set of modules available to a build,
132 use 'go list -m -json all'.
134 Edit also provides the -C, -n, and -x build flags.
136 See https://golang.org/ref/mod#go-mod-edit for more about 'go mod edit'.
141 editFmt = cmdEdit.Flag.Bool("fmt", false, "")
142 editGo = cmdEdit.Flag.String("go", "", "")
143 editToolchain = cmdEdit.Flag.String("toolchain", "", "")
144 editJSON = cmdEdit.Flag.Bool("json", false, "")
145 editPrint = cmdEdit.Flag.Bool("print", false, "")
146 editModule = cmdEdit.Flag.String("module", "", "")
147 edits []func(*modfile.File) // edits specified in flags
150 type flagFunc func(string)
152 func (f flagFunc) String() string { return "" }
153 func (f flagFunc) Set(s string) error { f(s); return nil }
156 cmdEdit.Run = runEdit // break init cycle
158 cmdEdit.Flag.Var(flagFunc(flagRequire), "require", "")
159 cmdEdit.Flag.Var(flagFunc(flagDropRequire), "droprequire", "")
160 cmdEdit.Flag.Var(flagFunc(flagExclude), "exclude", "")
161 cmdEdit.Flag.Var(flagFunc(flagDropReplace), "dropreplace", "")
162 cmdEdit.Flag.Var(flagFunc(flagReplace), "replace", "")
163 cmdEdit.Flag.Var(flagFunc(flagDropExclude), "dropexclude", "")
164 cmdEdit.Flag.Var(flagFunc(flagRetract), "retract", "")
165 cmdEdit.Flag.Var(flagFunc(flagDropRetract), "dropretract", "")
167 base.AddBuildFlagsNX(&cmdEdit.Flag)
168 base.AddChdirFlag(&cmdEdit.Flag)
169 base.AddModCommonFlags(&cmdEdit.Flag)
172 func runEdit(ctx context.Context, cmd *base.Command, args []string) {
173 anyFlags := *editModule != "" ||
175 *editToolchain != "" ||
182 base.Fatalf("go: no flags specified (see 'go help mod edit').")
185 if *editJSON && *editPrint {
186 base.Fatalf("go: cannot use both -json and -print")
190 base.Fatalf("go: too many arguments")
196 gomod = modload.ModFilePath()
199 if *editModule != "" {
200 if err := module.CheckImportPath(*editModule); err != nil {
201 base.Fatalf("go: invalid -module: %v", err)
205 if *editGo != "" && *editGo != "none" {
206 if !modfile.GoVersionRE.MatchString(*editGo) {
207 base.Fatalf(`go mod: invalid -go option; expecting something like "-go %s"`, gover.Local())
210 if *editToolchain != "" && *editToolchain != "none" {
211 if !modfile.ToolchainRE.MatchString(*editToolchain) {
212 base.Fatalf(`go mod: invalid -toolchain option; expecting something like "-toolchain go%s"`, gover.Local())
216 data, err := lockedfile.Read(gomod)
221 modFile, err := modfile.Parse(gomod, data, nil)
223 base.Fatalf("go: errors parsing %s:\n%s", base.ShortPath(gomod), err)
226 if *editModule != "" {
227 modFile.AddModuleStmt(*editModule)
230 if *editGo == "none" {
232 } else if *editGo != "" {
233 if err := modFile.AddGoStmt(*editGo); err != nil {
234 base.Fatalf("go: internal error: %v", err)
237 if *editToolchain == "none" {
238 modFile.DropToolchainStmt()
239 } else if *editToolchain != "" {
240 if err := modFile.AddToolchainStmt(*editToolchain); err != nil {
241 base.Fatalf("go: internal error: %v", err)
246 for _, edit := range edits {
251 modFile.Cleanup() // clean file after edits
254 editPrintJSON(modFile)
258 out, err := modFile.Format()
268 // Make a best-effort attempt to acquire the side lock, only to exclude
269 // previous versions of the 'go' command from making simultaneous edits.
270 if unlock, err := modfetch.SideLock(ctx); err == nil {
274 err = lockedfile.Transform(gomod, func(lockedData []byte) ([]byte, error) {
275 if !bytes.Equal(lockedData, data) {
276 return nil, errors.New("go.mod changed during editing; not overwriting")
285 // parsePathVersion parses -flag=arg expecting arg to be path@version.
286 func parsePathVersion(flag, arg string) (path, version string) {
287 before, after, found := strings.Cut(arg, "@")
289 base.Fatalf("go: -%s=%s: need path@version", flag, arg)
291 path, version = strings.TrimSpace(before), strings.TrimSpace(after)
292 if err := module.CheckImportPath(path); err != nil {
293 base.Fatalf("go: -%s=%s: invalid path: %v", flag, arg, err)
296 if !allowedVersionArg(version) {
297 base.Fatalf("go: -%s=%s: invalid version %q", flag, arg, version)
303 // parsePath parses -flag=arg expecting arg to be path (not path@version).
304 func parsePath(flag, arg string) (path string) {
305 if strings.Contains(arg, "@") {
306 base.Fatalf("go: -%s=%s: need just path, not path@version", flag, arg)
309 if err := module.CheckImportPath(path); err != nil {
310 base.Fatalf("go: -%s=%s: invalid path: %v", flag, arg, err)
315 // parsePathVersionOptional parses path[@version], using adj to
316 // describe any errors.
317 func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) {
318 if allowDirPath && modfile.IsDirectoryPath(arg) {
321 before, after, found := strings.Cut(arg, "@")
325 path, version = strings.TrimSpace(before), strings.TrimSpace(after)
327 if err := module.CheckImportPath(path); err != nil {
328 return path, version, fmt.Errorf("invalid %s path: %v", adj, err)
330 if path != arg && !allowedVersionArg(version) {
331 return path, version, fmt.Errorf("invalid %s version: %q", adj, version)
333 return path, version, nil
336 // parseVersionInterval parses a single version like "v1.2.3" or a closed
337 // interval like "[v1.2.3,v1.4.5]". Note that a single version has the same
338 // representation as an interval with equal upper and lower bounds: both
339 // Low and High are set.
340 func parseVersionInterval(arg string) (modfile.VersionInterval, error) {
341 if !strings.HasPrefix(arg, "[") {
342 if !allowedVersionArg(arg) {
343 return modfile.VersionInterval{}, fmt.Errorf("invalid version: %q", arg)
345 return modfile.VersionInterval{Low: arg, High: arg}, nil
347 if !strings.HasSuffix(arg, "]") {
348 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
350 s := arg[1 : len(arg)-1]
351 before, after, found := strings.Cut(s, ",")
353 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
355 low := strings.TrimSpace(before)
356 high := strings.TrimSpace(after)
357 if !allowedVersionArg(low) || !allowedVersionArg(high) {
358 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
360 return modfile.VersionInterval{Low: low, High: high}, nil
363 // allowedVersionArg returns whether a token may be used as a version in go.mod.
364 // We don't call modfile.CheckPathVersion, because that insists on versions
365 // being in semver form, but here we want to allow versions like "master" or
366 // "1234abcdef", which the go command will resolve the next time it runs (or
367 // during -fix). Even so, we need to make sure the version is a valid token.
368 func allowedVersionArg(arg string) bool {
369 return !modfile.MustQuote(arg)
372 // flagRequire implements the -require flag.
373 func flagRequire(arg string) {
374 path, version := parsePathVersion("require", arg)
375 edits = append(edits, func(f *modfile.File) {
376 if err := f.AddRequire(path, version); err != nil {
377 base.Fatalf("go: -require=%s: %v", arg, err)
382 // flagDropRequire implements the -droprequire flag.
383 func flagDropRequire(arg string) {
384 path := parsePath("droprequire", arg)
385 edits = append(edits, func(f *modfile.File) {
386 if err := f.DropRequire(path); err != nil {
387 base.Fatalf("go: -droprequire=%s: %v", arg, err)
392 // flagExclude implements the -exclude flag.
393 func flagExclude(arg string) {
394 path, version := parsePathVersion("exclude", arg)
395 edits = append(edits, func(f *modfile.File) {
396 if err := f.AddExclude(path, version); err != nil {
397 base.Fatalf("go: -exclude=%s: %v", arg, err)
402 // flagDropExclude implements the -dropexclude flag.
403 func flagDropExclude(arg string) {
404 path, version := parsePathVersion("dropexclude", arg)
405 edits = append(edits, func(f *modfile.File) {
406 if err := f.DropExclude(path, version); err != nil {
407 base.Fatalf("go: -dropexclude=%s: %v", arg, err)
412 // flagReplace implements the -replace flag.
413 func flagReplace(arg string) {
414 before, after, found := strings.Cut(arg, "=")
416 base.Fatalf("go: -replace=%s: need old[@v]=new[@w] (missing =)", arg)
418 old, new := strings.TrimSpace(before), strings.TrimSpace(after)
419 if strings.HasPrefix(new, ">") {
420 base.Fatalf("go: -replace=%s: separator between old and new is =, not =>", arg)
422 oldPath, oldVersion, err := parsePathVersionOptional("old", old, false)
424 base.Fatalf("go: -replace=%s: %v", arg, err)
426 newPath, newVersion, err := parsePathVersionOptional("new", new, true)
428 base.Fatalf("go: -replace=%s: %v", arg, err)
430 if newPath == new && !modfile.IsDirectoryPath(new) {
431 base.Fatalf("go: -replace=%s: unversioned new path must be local directory", arg)
434 edits = append(edits, func(f *modfile.File) {
435 if err := f.AddReplace(oldPath, oldVersion, newPath, newVersion); err != nil {
436 base.Fatalf("go: -replace=%s: %v", arg, err)
441 // flagDropReplace implements the -dropreplace flag.
442 func flagDropReplace(arg string) {
443 path, version, err := parsePathVersionOptional("old", arg, true)
445 base.Fatalf("go: -dropreplace=%s: %v", arg, err)
447 edits = append(edits, func(f *modfile.File) {
448 if err := f.DropReplace(path, version); err != nil {
449 base.Fatalf("go: -dropreplace=%s: %v", arg, err)
454 // flagRetract implements the -retract flag.
455 func flagRetract(arg string) {
456 vi, err := parseVersionInterval(arg)
458 base.Fatalf("go: -retract=%s: %v", arg, err)
460 edits = append(edits, func(f *modfile.File) {
461 if err := f.AddRetract(vi, ""); err != nil {
462 base.Fatalf("go: -retract=%s: %v", arg, err)
467 // flagDropRetract implements the -dropretract flag.
468 func flagDropRetract(arg string) {
469 vi, err := parseVersionInterval(arg)
471 base.Fatalf("go: -dropretract=%s: %v", arg, err)
473 edits = append(edits, func(f *modfile.File) {
474 if err := f.DropRetract(vi); err != nil {
475 base.Fatalf("go: -dropretract=%s: %v", arg, err)
480 // fileJSON is the -json output data structure.
481 type fileJSON struct {
482 Module editModuleJSON
483 Go string `json:",omitempty"`
484 Toolchain string `json:",omitempty"`
485 Require []requireJSON
486 Exclude []module.Version
487 Replace []replaceJSON
488 Retract []retractJSON
491 type editModuleJSON struct {
493 Deprecated string `json:",omitempty"`
496 type requireJSON struct {
498 Version string `json:",omitempty"`
499 Indirect bool `json:",omitempty"`
502 type replaceJSON struct {
507 type retractJSON struct {
508 Low string `json:",omitempty"`
509 High string `json:",omitempty"`
510 Rationale string `json:",omitempty"`
513 // editPrintJSON prints the -json output.
514 func editPrintJSON(modFile *modfile.File) {
516 if modFile.Module != nil {
517 f.Module = editModuleJSON{
518 Path: modFile.Module.Mod.Path,
519 Deprecated: modFile.Module.Deprecated,
522 if modFile.Go != nil {
523 f.Go = modFile.Go.Version
525 if modFile.Toolchain != nil {
526 f.Toolchain = modFile.Toolchain.Name
528 for _, r := range modFile.Require {
529 f.Require = append(f.Require, requireJSON{Path: r.Mod.Path, Version: r.Mod.Version, Indirect: r.Indirect})
531 for _, x := range modFile.Exclude {
532 f.Exclude = append(f.Exclude, x.Mod)
534 for _, r := range modFile.Replace {
535 f.Replace = append(f.Replace, replaceJSON{r.Old, r.New})
537 for _, r := range modFile.Retract {
538 f.Retract = append(f.Retract, retractJSON{r.Low, r.High, r.Rationale})
540 data, err := json.MarshalIndent(&f, "", "\t")
542 base.Fatalf("go: internal error: %v", err)
544 data = append(data, '\n')
545 os.Stdout.Write(data)