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 err := module.CheckImportPath(*editModule)
238 if err == nil {
239 err = modload.CheckReservedModulePath(*editModule)
240 }
241 if err != nil {
242 base.Fatalf("go: invalid -module: %v", err)
243 }
244 }
245
246 if *editGo != "" && *editGo != "none" {
247 if !modfile.GoVersionRE.MatchString(*editGo) {
248 base.Fatalf(`go mod: invalid -go option; expecting something like "-go %s"`, gover.Local())
249 }
250 }
251 if *editToolchain != "" && *editToolchain != "none" {
252 if !modfile.ToolchainRE.MatchString(*editToolchain) {
253 base.Fatalf(`go mod: invalid -toolchain option; expecting something like "-toolchain go%s"`, gover.Local())
254 }
255 }
256
257 data, err := lockedfile.Read(gomod)
258 if err != nil {
259 base.Fatal(err)
260 }
261
262 modFile, err := modfile.Parse(gomod, data, nil)
263 if err != nil {
264 base.Fatalf("go: errors parsing %s:\n%s", base.ShortPath(gomod), err)
265 }
266
267 if *editModule != "" {
268 modFile.AddModuleStmt(*editModule)
269 }
270
271 if *editGo == "none" {
272 modFile.DropGoStmt()
273 } else if *editGo != "" {
274 if err := modFile.AddGoStmt(*editGo); err != nil {
275 base.Fatalf("go: internal error: %v", err)
276 }
277 }
278 if *editToolchain == "none" {
279 modFile.DropToolchainStmt()
280 } else if *editToolchain != "" {
281 if err := modFile.AddToolchainStmt(*editToolchain); err != nil {
282 base.Fatalf("go: internal error: %v", err)
283 }
284 }
285
286 if len(edits) > 0 {
287 for _, edit := range edits {
288 edit(modFile)
289 }
290 }
291 modFile.SortBlocks()
292 modFile.Cleanup()
293
294 if *editJSON {
295 editPrintJSON(modFile)
296 return
297 }
298
299 out, err := modFile.Format()
300 if err != nil {
301 base.Fatal(err)
302 }
303
304 if *editPrint {
305 os.Stdout.Write(out)
306 return
307 }
308
309
310
311 if unlock, err := modfetch.SideLock(ctx); err == nil {
312 defer unlock()
313 }
314
315 err = lockedfile.Transform(gomod, func(lockedData []byte) ([]byte, error) {
316 if !bytes.Equal(lockedData, data) {
317 return nil, errors.New("go.mod changed during editing; not overwriting")
318 }
319 return out, nil
320 })
321 if err != nil {
322 base.Fatal(err)
323 }
324 }
325
326
327 func parsePathVersion(flag, arg string) (path, version string) {
328 before, after, found := strings.Cut(arg, "@")
329 if !found {
330 base.Fatalf("go: -%s=%s: need path@version", flag, arg)
331 }
332 path, version = strings.TrimSpace(before), strings.TrimSpace(after)
333 if err := module.CheckImportPath(path); err != nil {
334 base.Fatalf("go: -%s=%s: invalid path: %v", flag, arg, err)
335 }
336
337 if !allowedVersionArg(version) {
338 base.Fatalf("go: -%s=%s: invalid version %q", flag, arg, version)
339 }
340
341 return path, version
342 }
343
344
345 func parsePath(flag, arg string) (path string) {
346 if strings.Contains(arg, "@") {
347 base.Fatalf("go: -%s=%s: need just path, not path@version", flag, arg)
348 }
349 path = arg
350 if err := module.CheckImportPath(path); err != nil {
351 base.Fatalf("go: -%s=%s: invalid path: %v", flag, arg, err)
352 }
353 return path
354 }
355
356
357
358 func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) {
359 if allowDirPath && modfile.IsDirectoryPath(arg) {
360 return arg, "", nil
361 }
362 before, after, found := strings.Cut(arg, "@")
363 if !found {
364 path = arg
365 } else {
366 path, version = strings.TrimSpace(before), strings.TrimSpace(after)
367 }
368 if err := module.CheckImportPath(path); err != nil {
369 return path, version, fmt.Errorf("invalid %s path: %v", adj, err)
370 }
371 if path != arg && !allowedVersionArg(version) {
372 return path, version, fmt.Errorf("invalid %s version: %q", adj, version)
373 }
374 return path, version, nil
375 }
376
377
378
379
380
381 func parseVersionInterval(arg string) (modfile.VersionInterval, error) {
382 if !strings.HasPrefix(arg, "[") {
383 if !allowedVersionArg(arg) {
384 return modfile.VersionInterval{}, fmt.Errorf("invalid version: %q", arg)
385 }
386 return modfile.VersionInterval{Low: arg, High: arg}, nil
387 }
388 if !strings.HasSuffix(arg, "]") {
389 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
390 }
391 s := arg[1 : len(arg)-1]
392 before, after, found := strings.Cut(s, ",")
393 if !found {
394 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
395 }
396 low := strings.TrimSpace(before)
397 high := strings.TrimSpace(after)
398 if !allowedVersionArg(low) || !allowedVersionArg(high) {
399 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
400 }
401 return modfile.VersionInterval{Low: low, High: high}, nil
402 }
403
404
405
406
407
408
409 func allowedVersionArg(arg string) bool {
410 return !modfile.MustQuote(arg)
411 }
412
413
414 func flagGodebug(arg string) {
415 key, value, ok := strings.Cut(arg, "=")
416 if !ok || strings.ContainsAny(arg, "\"`',") {
417 base.Fatalf("go: -godebug=%s: need key=value", arg)
418 }
419 edits = append(edits, func(f *modfile.File) {
420 if err := f.AddGodebug(key, value); err != nil {
421 base.Fatalf("go: -godebug=%s: %v", arg, err)
422 }
423 })
424 }
425
426
427 func flagDropGodebug(arg string) {
428 edits = append(edits, func(f *modfile.File) {
429 if err := f.DropGodebug(arg); err != nil {
430 base.Fatalf("go: -dropgodebug=%s: %v", arg, err)
431 }
432 })
433 }
434
435
436 func flagRequire(arg string) {
437 path, version := parsePathVersion("require", arg)
438 edits = append(edits, func(f *modfile.File) {
439 if err := f.AddRequire(path, version); err != nil {
440 base.Fatalf("go: -require=%s: %v", arg, err)
441 }
442 })
443 }
444
445
446 func flagDropRequire(arg string) {
447 path := parsePath("droprequire", arg)
448 edits = append(edits, func(f *modfile.File) {
449 if err := f.DropRequire(path); err != nil {
450 base.Fatalf("go: -droprequire=%s: %v", arg, err)
451 }
452 })
453 }
454
455
456 func flagExclude(arg string) {
457 path, version := parsePathVersion("exclude", arg)
458 edits = append(edits, func(f *modfile.File) {
459 if err := f.AddExclude(path, version); err != nil {
460 base.Fatalf("go: -exclude=%s: %v", arg, err)
461 }
462 })
463 }
464
465
466 func flagDropExclude(arg string) {
467 path, version := parsePathVersion("dropexclude", arg)
468 edits = append(edits, func(f *modfile.File) {
469 if err := f.DropExclude(path, version); err != nil {
470 base.Fatalf("go: -dropexclude=%s: %v", arg, err)
471 }
472 })
473 }
474
475
476 func flagReplace(arg string) {
477 before, after, found := strings.Cut(arg, "=")
478 if !found {
479 base.Fatalf("go: -replace=%s: need old[@v]=new[@w] (missing =)", arg)
480 }
481 old, new := strings.TrimSpace(before), strings.TrimSpace(after)
482 if strings.HasPrefix(new, ">") {
483 base.Fatalf("go: -replace=%s: separator between old and new is =, not =>", arg)
484 }
485 oldPath, oldVersion, err := parsePathVersionOptional("old", old, false)
486 if err != nil {
487 base.Fatalf("go: -replace=%s: %v", arg, err)
488 }
489 newPath, newVersion, err := parsePathVersionOptional("new", new, true)
490 if err != nil {
491 base.Fatalf("go: -replace=%s: %v", arg, err)
492 }
493 if newPath == new && !modfile.IsDirectoryPath(new) {
494 base.Fatalf("go: -replace=%s: unversioned new path must be local directory", arg)
495 }
496
497 edits = append(edits, func(f *modfile.File) {
498 if err := f.AddReplace(oldPath, oldVersion, newPath, newVersion); err != nil {
499 base.Fatalf("go: -replace=%s: %v", arg, err)
500 }
501 })
502 }
503
504
505 func flagDropReplace(arg string) {
506 path, version, err := parsePathVersionOptional("old", arg, true)
507 if err != nil {
508 base.Fatalf("go: -dropreplace=%s: %v", arg, err)
509 }
510 edits = append(edits, func(f *modfile.File) {
511 if err := f.DropReplace(path, version); err != nil {
512 base.Fatalf("go: -dropreplace=%s: %v", arg, err)
513 }
514 })
515 }
516
517
518 func flagRetract(arg string) {
519 vi, err := parseVersionInterval(arg)
520 if err != nil {
521 base.Fatalf("go: -retract=%s: %v", arg, err)
522 }
523 edits = append(edits, func(f *modfile.File) {
524 if err := f.AddRetract(vi, ""); err != nil {
525 base.Fatalf("go: -retract=%s: %v", arg, err)
526 }
527 })
528 }
529
530
531 func flagDropRetract(arg string) {
532 vi, err := parseVersionInterval(arg)
533 if err != nil {
534 base.Fatalf("go: -dropretract=%s: %v", arg, err)
535 }
536 edits = append(edits, func(f *modfile.File) {
537 if err := f.DropRetract(vi); err != nil {
538 base.Fatalf("go: -dropretract=%s: %v", arg, err)
539 }
540 })
541 }
542
543
544 func flagTool(arg string) {
545 path := parsePath("tool", arg)
546 edits = append(edits, func(f *modfile.File) {
547 if err := f.AddTool(path); err != nil {
548 base.Fatalf("go: -tool=%s: %v", arg, err)
549 }
550 })
551 }
552
553
554 func flagDropTool(arg string) {
555 path := parsePath("droptool", arg)
556 edits = append(edits, func(f *modfile.File) {
557 if err := f.DropTool(path); err != nil {
558 base.Fatalf("go: -droptool=%s: %v", arg, err)
559 }
560 })
561 }
562
563
564 func flagIgnore(arg string) {
565 edits = append(edits, func(f *modfile.File) {
566 if err := f.AddIgnore(arg); err != nil {
567 base.Fatalf("go: -ignore=%s: %v", arg, err)
568 }
569 })
570 }
571
572
573 func flagDropIgnore(arg string) {
574 edits = append(edits, func(f *modfile.File) {
575 if err := f.DropIgnore(arg); err != nil {
576 base.Fatalf("go: -dropignore=%s: %v", arg, err)
577 }
578 })
579 }
580
581
582 type fileJSON struct {
583 Module editModuleJSON
584 Go string `json:",omitempty"`
585 Toolchain string `json:",omitempty"`
586 Require []requireJSON
587 Exclude []module.Version
588 Replace []replaceJSON
589 Retract []retractJSON
590 Tool []toolJSON
591 Ignore []ignoreJSON
592 }
593
594 type editModuleJSON struct {
595 Path string
596 Deprecated string `json:",omitempty"`
597 }
598
599 type requireJSON struct {
600 Path string
601 Version string `json:",omitempty"`
602 Indirect bool `json:",omitempty"`
603 }
604
605 type replaceJSON struct {
606 Old module.Version
607 New module.Version
608 }
609
610 type retractJSON struct {
611 Low string `json:",omitempty"`
612 High string `json:",omitempty"`
613 Rationale string `json:",omitempty"`
614 }
615
616 type toolJSON struct {
617 Path string
618 }
619
620 type ignoreJSON struct {
621 Path string
622 }
623
624
625 func editPrintJSON(modFile *modfile.File) {
626 var f fileJSON
627 if modFile.Module != nil {
628 f.Module = editModuleJSON{
629 Path: modFile.Module.Mod.Path,
630 Deprecated: modFile.Module.Deprecated,
631 }
632 }
633 if modFile.Go != nil {
634 f.Go = modFile.Go.Version
635 }
636 if modFile.Toolchain != nil {
637 f.Toolchain = modFile.Toolchain.Name
638 }
639 for _, r := range modFile.Require {
640 f.Require = append(f.Require, requireJSON{Path: r.Mod.Path, Version: r.Mod.Version, Indirect: r.Indirect})
641 }
642 for _, x := range modFile.Exclude {
643 f.Exclude = append(f.Exclude, x.Mod)
644 }
645 for _, r := range modFile.Replace {
646 f.Replace = append(f.Replace, replaceJSON{r.Old, r.New})
647 }
648 for _, r := range modFile.Retract {
649 f.Retract = append(f.Retract, retractJSON{r.Low, r.High, r.Rationale})
650 }
651 for _, t := range modFile.Tool {
652 f.Tool = append(f.Tool, toolJSON{t.Path})
653 }
654 for _, i := range modFile.Ignore {
655 f.Ignore = append(f.Ignore, ignoreJSON{i.Path})
656 }
657 data, err := json.MarshalIndent(&f, "", "\t")
658 if err != nil {
659 base.Fatalf("go: internal error: %v", err)
660 }
661 data = append(data, '\n')
662 os.Stdout.Write(data)
663 }
664
View as plain text