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 := strings.Cut(arg, "@")
332 if !found {
333 base.Fatalf("go: -%s=%s: need path@version", flag, arg)
334 }
335 path, version = strings.TrimSpace(before), strings.TrimSpace(after)
336 if err := module.CheckImportPath(path); err != nil {
337 base.Fatalf("go: -%s=%s: invalid path: %v", flag, arg, err)
338 }
339
340 if !allowedVersionArg(version) {
341 base.Fatalf("go: -%s=%s: invalid version %q", flag, arg, version)
342 }
343
344 return path, version
345 }
346
347
348 func parsePath(flag, arg string) (path string) {
349 if strings.Contains(arg, "@") {
350 base.Fatalf("go: -%s=%s: need just path, not path@version", flag, arg)
351 }
352 path = arg
353 if err := module.CheckImportPath(path); err != nil {
354 base.Fatalf("go: -%s=%s: invalid path: %v", flag, arg, err)
355 }
356 return path
357 }
358
359
360
361 func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) {
362 if allowDirPath && modfile.IsDirectoryPath(arg) {
363 return arg, "", nil
364 }
365 before, after, found := strings.Cut(arg, "@")
366 if !found {
367 path = arg
368 } else {
369 path, version = strings.TrimSpace(before), strings.TrimSpace(after)
370 }
371 if err := module.CheckImportPath(path); err != nil {
372 return path, version, fmt.Errorf("invalid %s path: %v", adj, err)
373 }
374 if path != arg && !allowedVersionArg(version) {
375 return path, version, fmt.Errorf("invalid %s version: %q", adj, version)
376 }
377 return path, version, nil
378 }
379
380
381
382
383
384 func parseVersionInterval(arg string) (modfile.VersionInterval, error) {
385 if !strings.HasPrefix(arg, "[") {
386 if !allowedVersionArg(arg) {
387 return modfile.VersionInterval{}, fmt.Errorf("invalid version: %q", arg)
388 }
389 return modfile.VersionInterval{Low: arg, High: arg}, nil
390 }
391 if !strings.HasSuffix(arg, "]") {
392 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
393 }
394 s := arg[1 : len(arg)-1]
395 before, after, found := strings.Cut(s, ",")
396 if !found {
397 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
398 }
399 low := strings.TrimSpace(before)
400 high := strings.TrimSpace(after)
401 if !allowedVersionArg(low) || !allowedVersionArg(high) {
402 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
403 }
404 return modfile.VersionInterval{Low: low, High: high}, nil
405 }
406
407
408
409
410
411
412 func allowedVersionArg(arg string) bool {
413 return !modfile.MustQuote(arg)
414 }
415
416
417 func flagGodebug(arg string) {
418 key, value, ok := strings.Cut(arg, "=")
419 if !ok || strings.ContainsAny(arg, "\"`',") {
420 base.Fatalf("go: -godebug=%s: need key=value", arg)
421 }
422 edits = append(edits, func(f *modfile.File) {
423 if err := f.AddGodebug(key, value); err != nil {
424 base.Fatalf("go: -godebug=%s: %v", arg, err)
425 }
426 })
427 }
428
429
430 func flagDropGodebug(arg string) {
431 edits = append(edits, func(f *modfile.File) {
432 if err := f.DropGodebug(arg); err != nil {
433 base.Fatalf("go: -dropgodebug=%s: %v", arg, err)
434 }
435 })
436 }
437
438
439 func flagRequire(arg string) {
440 path, version := parsePathVersion("require", arg)
441 edits = append(edits, func(f *modfile.File) {
442 if err := f.AddRequire(path, version); err != nil {
443 base.Fatalf("go: -require=%s: %v", arg, err)
444 }
445 })
446 }
447
448
449 func flagDropRequire(arg string) {
450 path := parsePath("droprequire", arg)
451 edits = append(edits, func(f *modfile.File) {
452 if err := f.DropRequire(path); err != nil {
453 base.Fatalf("go: -droprequire=%s: %v", arg, err)
454 }
455 })
456 }
457
458
459 func flagExclude(arg string) {
460 path, version := parsePathVersion("exclude", arg)
461 edits = append(edits, func(f *modfile.File) {
462 if err := f.AddExclude(path, version); err != nil {
463 base.Fatalf("go: -exclude=%s: %v", arg, err)
464 }
465 })
466 }
467
468
469 func flagDropExclude(arg string) {
470 path, version := parsePathVersion("dropexclude", arg)
471 edits = append(edits, func(f *modfile.File) {
472 if err := f.DropExclude(path, version); err != nil {
473 base.Fatalf("go: -dropexclude=%s: %v", arg, err)
474 }
475 })
476 }
477
478
479 func flagReplace(arg string) {
480 before, after, found := strings.Cut(arg, "=")
481 if !found {
482 base.Fatalf("go: -replace=%s: need old[@v]=new[@w] (missing =)", arg)
483 }
484 old, new := strings.TrimSpace(before), strings.TrimSpace(after)
485 if strings.HasPrefix(new, ">") {
486 base.Fatalf("go: -replace=%s: separator between old and new is =, not =>", arg)
487 }
488 oldPath, oldVersion, err := parsePathVersionOptional("old", old, false)
489 if err != nil {
490 base.Fatalf("go: -replace=%s: %v", arg, err)
491 }
492 newPath, newVersion, err := parsePathVersionOptional("new", new, true)
493 if err != nil {
494 base.Fatalf("go: -replace=%s: %v", arg, err)
495 }
496 if newPath == new && !modfile.IsDirectoryPath(new) {
497 base.Fatalf("go: -replace=%s: unversioned new path must be local directory", arg)
498 }
499
500 edits = append(edits, func(f *modfile.File) {
501 if err := f.AddReplace(oldPath, oldVersion, newPath, newVersion); err != nil {
502 base.Fatalf("go: -replace=%s: %v", arg, err)
503 }
504 })
505 }
506
507
508 func flagDropReplace(arg string) {
509 path, version, err := parsePathVersionOptional("old", arg, true)
510 if err != nil {
511 base.Fatalf("go: -dropreplace=%s: %v", arg, err)
512 }
513 edits = append(edits, func(f *modfile.File) {
514 if err := f.DropReplace(path, version); err != nil {
515 base.Fatalf("go: -dropreplace=%s: %v", arg, err)
516 }
517 })
518 }
519
520
521 func flagRetract(arg string) {
522 vi, err := parseVersionInterval(arg)
523 if err != nil {
524 base.Fatalf("go: -retract=%s: %v", arg, err)
525 }
526 edits = append(edits, func(f *modfile.File) {
527 if err := f.AddRetract(vi, ""); err != nil {
528 base.Fatalf("go: -retract=%s: %v", arg, err)
529 }
530 })
531 }
532
533
534 func flagDropRetract(arg string) {
535 vi, err := parseVersionInterval(arg)
536 if err != nil {
537 base.Fatalf("go: -dropretract=%s: %v", arg, err)
538 }
539 edits = append(edits, func(f *modfile.File) {
540 if err := f.DropRetract(vi); err != nil {
541 base.Fatalf("go: -dropretract=%s: %v", arg, err)
542 }
543 })
544 }
545
546
547 func flagTool(arg string) {
548 path := parsePath("tool", arg)
549 edits = append(edits, func(f *modfile.File) {
550 if err := f.AddTool(path); err != nil {
551 base.Fatalf("go: -tool=%s: %v", arg, err)
552 }
553 })
554 }
555
556
557 func flagDropTool(arg string) {
558 path := parsePath("droptool", arg)
559 edits = append(edits, func(f *modfile.File) {
560 if err := f.DropTool(path); err != nil {
561 base.Fatalf("go: -droptool=%s: %v", arg, err)
562 }
563 })
564 }
565
566
567 func flagIgnore(arg string) {
568 edits = append(edits, func(f *modfile.File) {
569 if err := f.AddIgnore(arg); err != nil {
570 base.Fatalf("go: -ignore=%s: %v", arg, err)
571 }
572 })
573 }
574
575
576 func flagDropIgnore(arg string) {
577 edits = append(edits, func(f *modfile.File) {
578 if err := f.DropIgnore(arg); err != nil {
579 base.Fatalf("go: -dropignore=%s: %v", arg, err)
580 }
581 })
582 }
583
584
585 type fileJSON struct {
586 Module editModuleJSON
587 Go string `json:",omitempty"`
588 Toolchain string `json:",omitempty"`
589 Require []requireJSON
590 Exclude []module.Version
591 Replace []replaceJSON
592 Retract []retractJSON
593 Tool []toolJSON
594 Ignore []ignoreJSON
595 }
596
597 type editModuleJSON struct {
598 Path string
599 Deprecated string `json:",omitempty"`
600 }
601
602 type requireJSON struct {
603 Path string
604 Version string `json:",omitempty"`
605 Indirect bool `json:",omitempty"`
606 }
607
608 type replaceJSON struct {
609 Old module.Version
610 New module.Version
611 }
612
613 type retractJSON struct {
614 Low string `json:",omitempty"`
615 High string `json:",omitempty"`
616 Rationale string `json:",omitempty"`
617 }
618
619 type toolJSON struct {
620 Path string
621 }
622
623 type ignoreJSON struct {
624 Path string
625 }
626
627
628 func editPrintJSON(modFile *modfile.File) {
629 var f fileJSON
630 if modFile.Module != nil {
631 f.Module = editModuleJSON{
632 Path: modFile.Module.Mod.Path,
633 Deprecated: modFile.Module.Deprecated,
634 }
635 }
636 if modFile.Go != nil {
637 f.Go = modFile.Go.Version
638 }
639 if modFile.Toolchain != nil {
640 f.Toolchain = modFile.Toolchain.Name
641 }
642 for _, r := range modFile.Require {
643 f.Require = append(f.Require, requireJSON{Path: r.Mod.Path, Version: r.Mod.Version, Indirect: r.Indirect})
644 }
645 for _, x := range modFile.Exclude {
646 f.Exclude = append(f.Exclude, x.Mod)
647 }
648 for _, r := range modFile.Replace {
649 f.Replace = append(f.Replace, replaceJSON{r.Old, r.New})
650 }
651 for _, r := range modFile.Retract {
652 f.Retract = append(f.Retract, retractJSON{r.Low, r.High, r.Rationale})
653 }
654 for _, t := range modFile.Tool {
655 f.Tool = append(f.Tool, toolJSON{t.Path})
656 }
657 for _, i := range modFile.Ignore {
658 f.Ignore = append(f.Ignore, ignoreJSON{i.Path})
659 }
660 data, err := json.MarshalIndent(&f, "", "\t")
661 if err != nil {
662 base.Fatalf("go: internal error: %v", err)
663 }
664 data = append(data, '\n')
665 os.Stdout.Write(data)
666 }
667
View as plain text