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