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