1
2
3
4
5
6
7 package modcmd
8
9 import (
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
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",
31 Long: `
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.
37
38 The editing flags specify a sequence of editing operations.
39
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'.
44
45 The -module flag changes the module's path (the go.mod file's module line).
46
47 The -godebug=key=value flag adds a godebug key=value line,
48 replacing any existing godebug lines with the given key.
49
50 The -dropgodebug=key flag drops any existing godebug lines
51 with the given key.
52
53 The -require=path@version and -droprequire=path flags
54 add and drop a requirement on the given module path and version.
55 Note that -require overrides any existing requirements on path.
56 These flags are mainly for tools that understand the module graph.
57 Users should prefer 'go get path@version' or 'go get path@none',
58 which make other go.mod adjustments as needed to satisfy
59 constraints imposed by other modules.
60
61 The -go=version flag sets the expected Go language version.
62 This flag is mainly for tools that understand Go version dependencies.
63 It takes a version like "1.26" or "1.26.2".
64 Using the version "none" removes the go directive.
65 Users should prefer 'go get go@version'.
66
67 The -toolchain=name flag sets the Go toolchain to use.
68 This flag is mainly for tools that understand Go version dependencies.
69 It takes a toolchain name like "go1.26" or "go1.26.2".
70 Using the name "none" removes the toolchain directive.
71 Users should prefer 'go get toolchain@version'.
72
73 The -exclude=path@version and -dropexclude=path@version flags
74 add and drop an exclusion for the given module path and version.
75 Note that -exclude=path@version is a no-op if that exclusion already exists.
76
77 The -replace=old[@v]=new[@v] flag adds a replacement of the given
78 module path and version pair. If the @v in old@v is omitted, a
79 replacement without a version on the left side is added, which applies
80 to all versions of the old module path. If the @v in new@v is omitted,
81 the new path should be a local module root directory, not a module
82 path. Note that -replace overrides any redundant replacements for old[@v],
83 so omitting @v will drop existing replacements for specific versions.
84
85 The -dropreplace=old[@v] flag drops a replacement of the given
86 module path and version pair. If the @v is omitted, a replacement without
87 a version on the left side is dropped.
88
89 The -retract=version and -dropretract=version flags add and drop a
90 retraction on the given version. The version may be a single version
91 like "v1.2.3" or a closed interval like "[v1.1.0,v1.1.9]". Note that
92 -retract=version is a no-op if that retraction already exists.
93
94 The -tool=path and -droptool=path flags add and drop a tool declaration
95 for the given path.
96
97 The -ignore=path and -dropignore=path flags add and drop a ignore declaration
98 for the given path.
99
100 The -godebug, -dropgodebug, -require, -droprequire, -exclude, -dropexclude,
101 -replace, -dropreplace, -retract, -dropretract, -tool, -droptool, -ignore,
102 and -dropignore editing flags may be repeated, and the changes are applied
103 in the order given.
104
105 The -print flag prints the final go.mod in its text format instead of
106 writing it back to go.mod.
107
108 The -json flag prints the final go.mod file in JSON format instead of
109 writing it back to go.mod. The JSON output corresponds to these Go types:
110
111 type GoMod struct {
112 Module ModPath
113 Go string
114 Toolchain string
115 Godebug []Godebug
116 Require []Require
117 Exclude []Module
118 Replace []Replace
119 Retract []Retract
120 Tool []Tool
121 Ignore []Ignore
122 }
123
124 type Module struct {
125 Path string
126 Version string
127 }
128
129 type ModPath struct {
130 Path string
131 Deprecated string
132 }
133
134 type Godebug struct {
135 Key string
136 Value string
137 }
138
139 type Require struct {
140 Path string
141 Version string
142 Indirect bool
143 }
144
145 type Replace struct {
146 Old Module
147 New Module
148 }
149
150 type Retract struct {
151 Low string
152 High string
153 Rationale string
154 }
155
156 type Tool struct {
157 Path string
158 }
159
160 type Ignore struct {
161 Path string
162 }
163
164 Retract entries representing a single version (not an interval) will have
165 the "Low" and "High" fields set to the same value.
166
167 Note that this only describes the go.mod file itself, not other modules
168 referred to indirectly. For the full set of modules available to a build,
169 use 'go list -m -json all'.
170
171 Edit also provides the -C, -n, and -x build flags.
172
173 See https://go.dev/ref/mod#go-mod-edit for more about 'go mod edit'.
174 `,
175 }
176
177 var (
178 editFmt = cmdEdit.Flag.Bool("fmt", false, "")
179 editGo = cmdEdit.Flag.String("go", "", "")
180 editToolchain = cmdEdit.Flag.String("toolchain", "", "")
181 editJSON = cmdEdit.Flag.Bool("json", false, "")
182 editPrint = cmdEdit.Flag.Bool("print", false, "")
183 editModule = cmdEdit.Flag.String("module", "", "")
184 edits []func(*modfile.File)
185 )
186
187 type flagFunc func(string)
188
189 func (f flagFunc) String() string { return "" }
190 func (f flagFunc) Set(s string) error { f(s); return nil }
191
192 func init() {
193 cmdEdit.Run = runEdit
194
195 cmdEdit.Flag.Var(flagFunc(flagGodebug), "godebug", "")
196 cmdEdit.Flag.Var(flagFunc(flagDropGodebug), "dropgodebug", "")
197 cmdEdit.Flag.Var(flagFunc(flagRequire), "require", "")
198 cmdEdit.Flag.Var(flagFunc(flagDropRequire), "droprequire", "")
199 cmdEdit.Flag.Var(flagFunc(flagExclude), "exclude", "")
200 cmdEdit.Flag.Var(flagFunc(flagDropExclude), "dropexclude", "")
201 cmdEdit.Flag.Var(flagFunc(flagReplace), "replace", "")
202 cmdEdit.Flag.Var(flagFunc(flagDropReplace), "dropreplace", "")
203 cmdEdit.Flag.Var(flagFunc(flagRetract), "retract", "")
204 cmdEdit.Flag.Var(flagFunc(flagDropRetract), "dropretract", "")
205 cmdEdit.Flag.Var(flagFunc(flagTool), "tool", "")
206 cmdEdit.Flag.Var(flagFunc(flagDropTool), "droptool", "")
207 cmdEdit.Flag.Var(flagFunc(flagIgnore), "ignore", "")
208 cmdEdit.Flag.Var(flagFunc(flagDropIgnore), "dropignore", "")
209
210 base.AddBuildFlagsNX(&cmdEdit.Flag)
211 base.AddChdirFlag(&cmdEdit.Flag)
212 base.AddModCommonFlags(&cmdEdit.Flag)
213 }
214
215 func runEdit(ctx context.Context, cmd *base.Command, args []string) {
216 moduleLoader := modload.NewLoader()
217 anyFlags := *editModule != "" ||
218 *editGo != "" ||
219 *editToolchain != "" ||
220 *editJSON ||
221 *editPrint ||
222 *editFmt ||
223 len(edits) > 0
224
225 if !anyFlags {
226 base.Fatalf("go: no flags specified (see 'go help mod edit').")
227 }
228
229 if *editJSON && *editPrint {
230 base.Fatalf("go: cannot use both -json and -print")
231 }
232
233 if len(args) > 1 {
234 base.Fatalf("go: too many arguments")
235 }
236 var gomod string
237 if len(args) == 1 {
238 gomod = args[0]
239 } else {
240 gomod = moduleLoader.ModFilePath()
241 }
242
243 if *editModule != "" {
244 err := module.CheckImportPath(*editModule)
245 if err == nil {
246 err = modload.CheckReservedModulePath(*editModule)
247 }
248 if err != nil {
249 base.Fatalf("go: invalid -module: %v", err)
250 }
251 }
252
253 if *editGo != "" && *editGo != "none" {
254 if !modfile.GoVersionRE.MatchString(*editGo) {
255 base.Fatalf(`go mod: invalid -go option; expecting something like "-go %s"`, gover.Local())
256 }
257 }
258 if *editToolchain != "" && *editToolchain != "none" {
259 if !modfile.ToolchainRE.MatchString(*editToolchain) {
260 base.Fatalf(`go mod: invalid -toolchain option; expecting something like "-toolchain go%s"`, gover.Local())
261 }
262 }
263
264 data, err := lockedfile.Read(gomod)
265 if err != nil {
266 base.Fatal(err)
267 }
268
269 modFile, err := modfile.Parse(gomod, data, nil)
270 if err != nil {
271 base.Fatalf("go: errors parsing %s:\n%s", base.ShortPath(gomod), err)
272 }
273
274 if *editModule != "" {
275 modFile.AddModuleStmt(*editModule)
276 }
277
278 if *editGo == "none" {
279 modFile.DropGoStmt()
280 } else if *editGo != "" {
281 if err := modFile.AddGoStmt(*editGo); err != nil {
282 base.Fatalf("go: internal error: %v", err)
283 }
284 }
285 if *editToolchain == "none" {
286 modFile.DropToolchainStmt()
287 } else if *editToolchain != "" {
288 if err := modFile.AddToolchainStmt(*editToolchain); err != nil {
289 base.Fatalf("go: internal error: %v", err)
290 }
291 }
292
293 if len(edits) > 0 {
294 for _, edit := range edits {
295 edit(modFile)
296 }
297 }
298 modFile.SortBlocks()
299 modFile.Cleanup()
300
301 if *editJSON {
302 editPrintJSON(modFile)
303 return
304 }
305
306 out, err := modFile.Format()
307 if err != nil {
308 base.Fatal(err)
309 }
310
311 if *editPrint {
312 os.Stdout.Write(out)
313 return
314 }
315
316
317
318 if unlock, err := modfetch.SideLock(ctx); err == nil {
319 defer unlock()
320 }
321
322 err = lockedfile.Transform(gomod, func(lockedData []byte) ([]byte, error) {
323 if !bytes.Equal(lockedData, data) {
324 return nil, errors.New("go.mod changed during editing; not overwriting")
325 }
326 return out, nil
327 })
328 if err != nil {
329 base.Fatal(err)
330 }
331 }
332
333
334 func parsePathVersion(flag, arg string) (path, version string) {
335 before, after, found, err := modload.ParsePathVersion(arg)
336 if err != nil {
337 base.Fatalf("go: -%s=%s: %v", flag, arg, err)
338 }
339 if !found {
340 base.Fatalf("go: -%s=%s: need path@version", flag, arg)
341 }
342 path, version = strings.TrimSpace(before), strings.TrimSpace(after)
343 if err := module.CheckImportPath(path); err != nil {
344 base.Fatalf("go: -%s=%s: invalid path: %v", flag, arg, err)
345 }
346
347 if !allowedVersionArg(version) {
348 base.Fatalf("go: -%s=%s: invalid version %q", flag, arg, version)
349 }
350
351 return path, version
352 }
353
354
355 func parsePath(flag, arg string) (path string) {
356 if strings.Contains(arg, "@") {
357 base.Fatalf("go: -%s=%s: need just path, not path@version", flag, arg)
358 }
359 path = arg
360 if err := module.CheckImportPath(path); err != nil {
361 base.Fatalf("go: -%s=%s: invalid path: %v", flag, arg, err)
362 }
363 return path
364 }
365
366
367
368 func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) {
369 if allowDirPath && modfile.IsDirectoryPath(arg) {
370 return arg, "", nil
371 }
372 before, after, found, err := modload.ParsePathVersion(arg)
373 if err != nil {
374 return "", "", err
375 }
376 if !found {
377 path = arg
378 } else {
379 path, version = strings.TrimSpace(before), strings.TrimSpace(after)
380 }
381 if err := module.CheckImportPath(path); err != nil {
382 return path, version, fmt.Errorf("invalid %s path: %v", adj, err)
383 }
384 if path != arg && !allowedVersionArg(version) {
385 return path, version, fmt.Errorf("invalid %s version: %q", adj, version)
386 }
387 return path, version, nil
388 }
389
390
391
392
393
394 func parseVersionInterval(arg string) (modfile.VersionInterval, error) {
395 if !strings.HasPrefix(arg, "[") {
396 if !allowedVersionArg(arg) {
397 return modfile.VersionInterval{}, fmt.Errorf("invalid version: %q", arg)
398 }
399 return modfile.VersionInterval{Low: arg, High: arg}, nil
400 }
401 if !strings.HasSuffix(arg, "]") {
402 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
403 }
404 s := arg[1 : len(arg)-1]
405 before, after, found := strings.Cut(s, ",")
406 if !found {
407 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
408 }
409 low := strings.TrimSpace(before)
410 high := strings.TrimSpace(after)
411 if !allowedVersionArg(low) || !allowedVersionArg(high) {
412 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
413 }
414 return modfile.VersionInterval{Low: low, High: high}, nil
415 }
416
417
418
419
420
421
422 func allowedVersionArg(arg string) bool {
423 return !modfile.MustQuote(arg)
424 }
425
426
427 func flagGodebug(arg string) {
428 key, value, ok := strings.Cut(arg, "=")
429 if !ok || strings.ContainsAny(arg, "\"`',") {
430 base.Fatalf("go: -godebug=%s: need key=value", arg)
431 }
432 edits = append(edits, func(f *modfile.File) {
433 if err := f.AddGodebug(key, value); err != nil {
434 base.Fatalf("go: -godebug=%s: %v", arg, err)
435 }
436 })
437 }
438
439
440 func flagDropGodebug(arg string) {
441 edits = append(edits, func(f *modfile.File) {
442 if err := f.DropGodebug(arg); err != nil {
443 base.Fatalf("go: -dropgodebug=%s: %v", arg, err)
444 }
445 })
446 }
447
448
449 func flagRequire(arg string) {
450 path, version := parsePathVersion("require", arg)
451 edits = append(edits, func(f *modfile.File) {
452 if err := f.AddRequire(path, version); err != nil {
453 base.Fatalf("go: -require=%s: %v", arg, err)
454 }
455 })
456 }
457
458
459 func flagDropRequire(arg string) {
460 path := parsePath("droprequire", arg)
461 edits = append(edits, func(f *modfile.File) {
462 if err := f.DropRequire(path); err != nil {
463 base.Fatalf("go: -droprequire=%s: %v", arg, err)
464 }
465 })
466 }
467
468
469 func flagExclude(arg string) {
470 path, version := parsePathVersion("exclude", arg)
471 edits = append(edits, func(f *modfile.File) {
472 if err := f.AddExclude(path, version); err != nil {
473 base.Fatalf("go: -exclude=%s: %v", arg, err)
474 }
475 })
476 }
477
478
479 func flagDropExclude(arg string) {
480 path, version := parsePathVersion("dropexclude", arg)
481 edits = append(edits, func(f *modfile.File) {
482 if err := f.DropExclude(path, version); err != nil {
483 base.Fatalf("go: -dropexclude=%s: %v", arg, err)
484 }
485 })
486 }
487
488
489 func flagReplace(arg string) {
490 before, after, found := strings.Cut(arg, "=")
491 if !found {
492 base.Fatalf("go: -replace=%s: need old[@v]=new[@w] (missing =)", arg)
493 }
494 old, new := strings.TrimSpace(before), strings.TrimSpace(after)
495 if strings.HasPrefix(new, ">") {
496 base.Fatalf("go: -replace=%s: separator between old and new is =, not =>", arg)
497 }
498 oldPath, oldVersion, err := parsePathVersionOptional("old", old, false)
499 if err != nil {
500 base.Fatalf("go: -replace=%s: %v", arg, err)
501 }
502 newPath, newVersion, err := parsePathVersionOptional("new", new, true)
503 if err != nil {
504 base.Fatalf("go: -replace=%s: %v", arg, err)
505 }
506 if newPath == new && !modfile.IsDirectoryPath(new) {
507 base.Fatalf("go: -replace=%s: unversioned new path must be local directory", arg)
508 }
509
510 edits = append(edits, func(f *modfile.File) {
511 if err := f.AddReplace(oldPath, oldVersion, newPath, newVersion); err != nil {
512 base.Fatalf("go: -replace=%s: %v", arg, err)
513 }
514 })
515 }
516
517
518 func flagDropReplace(arg string) {
519 path, version, err := parsePathVersionOptional("old", arg, true)
520 if err != nil {
521 base.Fatalf("go: -dropreplace=%s: %v", arg, err)
522 }
523 edits = append(edits, func(f *modfile.File) {
524 if err := f.DropReplace(path, version); err != nil {
525 base.Fatalf("go: -dropreplace=%s: %v", arg, err)
526 }
527 })
528 }
529
530
531 func flagRetract(arg string) {
532 vi, err := parseVersionInterval(arg)
533 if err != nil {
534 base.Fatalf("go: -retract=%s: %v", arg, err)
535 }
536 edits = append(edits, func(f *modfile.File) {
537 if err := f.AddRetract(vi, ""); err != nil {
538 base.Fatalf("go: -retract=%s: %v", arg, err)
539 }
540 })
541 }
542
543
544 func flagDropRetract(arg string) {
545 vi, err := parseVersionInterval(arg)
546 if err != nil {
547 base.Fatalf("go: -dropretract=%s: %v", arg, err)
548 }
549 edits = append(edits, func(f *modfile.File) {
550 if err := f.DropRetract(vi); err != nil {
551 base.Fatalf("go: -dropretract=%s: %v", arg, err)
552 }
553 })
554 }
555
556
557 func flagTool(arg string) {
558 path := parsePath("tool", arg)
559 edits = append(edits, func(f *modfile.File) {
560 if err := f.AddTool(path); err != nil {
561 base.Fatalf("go: -tool=%s: %v", arg, err)
562 }
563 })
564 }
565
566
567 func flagDropTool(arg string) {
568 path := parsePath("droptool", arg)
569 edits = append(edits, func(f *modfile.File) {
570 if err := f.DropTool(path); err != nil {
571 base.Fatalf("go: -droptool=%s: %v", arg, err)
572 }
573 })
574 }
575
576
577 func flagIgnore(arg string) {
578 edits = append(edits, func(f *modfile.File) {
579 if err := f.AddIgnore(arg); err != nil {
580 base.Fatalf("go: -ignore=%s: %v", arg, err)
581 }
582 })
583 }
584
585
586 func flagDropIgnore(arg string) {
587 edits = append(edits, func(f *modfile.File) {
588 if err := f.DropIgnore(arg); err != nil {
589 base.Fatalf("go: -dropignore=%s: %v", arg, err)
590 }
591 })
592 }
593
594
595 type fileJSON struct {
596 Module editModuleJSON
597 Go string `json:",omitempty"`
598 Toolchain string `json:",omitempty"`
599 GoDebug []debugJSON `json:",omitempty"`
600 Require []requireJSON `json:",omitempty"`
601 Exclude []module.Version `json:",omitempty"`
602 Replace []replaceJSON `json:",omitempty"`
603 Retract []retractJSON `json:",omitempty"`
604 Tool []toolJSON `json:",omitempty"`
605 Ignore []ignoreJSON `json:",omitempty"`
606 }
607
608 type editModuleJSON struct {
609 Path string
610 Deprecated string `json:",omitempty"`
611 }
612
613 type debugJSON struct {
614 Key string
615 Value string
616 }
617
618 type requireJSON struct {
619 Path string
620 Version string `json:",omitempty"`
621 Indirect bool `json:",omitempty"`
622 }
623
624 type replaceJSON struct {
625 Old module.Version
626 New module.Version
627 }
628
629 type retractJSON struct {
630 Low string `json:",omitempty"`
631 High string `json:",omitempty"`
632 Rationale string `json:",omitempty"`
633 }
634
635 type toolJSON struct {
636 Path string
637 }
638
639 type ignoreJSON struct {
640 Path string
641 }
642
643
644 func editPrintJSON(modFile *modfile.File) {
645 var f fileJSON
646 if modFile.Module != nil {
647 f.Module = editModuleJSON{
648 Path: modFile.Module.Mod.Path,
649 Deprecated: modFile.Module.Deprecated,
650 }
651 }
652 if modFile.Go != nil {
653 f.Go = modFile.Go.Version
654 }
655 if modFile.Toolchain != nil {
656 f.Toolchain = modFile.Toolchain.Name
657 }
658 for _, r := range modFile.Require {
659 f.Require = append(f.Require, requireJSON{Path: r.Mod.Path, Version: r.Mod.Version, Indirect: r.Indirect})
660 }
661 for _, x := range modFile.Exclude {
662 f.Exclude = append(f.Exclude, x.Mod)
663 }
664 for _, r := range modFile.Replace {
665 f.Replace = append(f.Replace, replaceJSON{r.Old, r.New})
666 }
667 for _, r := range modFile.Retract {
668 f.Retract = append(f.Retract, retractJSON{r.Low, r.High, r.Rationale})
669 }
670 for _, t := range modFile.Tool {
671 f.Tool = append(f.Tool, toolJSON{t.Path})
672 }
673 for _, i := range modFile.Ignore {
674 f.Ignore = append(f.Ignore, ignoreJSON{i.Path})
675 }
676 for _, d := range modFile.Godebug {
677 f.GoDebug = append(f.GoDebug, debugJSON{d.Key, d.Value})
678 }
679 data, err := json.MarshalIndent(&f, "", "\t")
680 if err != nil {
681 base.Fatalf("go: internal error: %v", err)
682 }
683 data = append(data, '\n')
684 os.Stdout.Write(data)
685 }
686
View as plain text