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