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 anyFlags := *editModule != "" ||
213 *editGo != "" ||
214 *editToolchain != "" ||
215 *editJSON ||
216 *editPrint ||
217 *editFmt ||
218 len(edits) > 0
219
220 if !anyFlags {
221 base.Fatalf("go: no flags specified (see 'go help mod edit').")
222 }
223
224 if *editJSON && *editPrint {
225 base.Fatalf("go: cannot use both -json and -print")
226 }
227
228 if len(args) > 1 {
229 base.Fatalf("go: too many arguments")
230 }
231 var gomod string
232 if len(args) == 1 {
233 gomod = args[0]
234 } else {
235 gomod = modload.ModFilePath()
236 }
237
238 if *editModule != "" {
239 err := module.CheckImportPath(*editModule)
240 if err == nil {
241 err = modload.CheckReservedModulePath(*editModule)
242 }
243 if err != nil {
244 base.Fatalf("go: invalid -module: %v", err)
245 }
246 }
247
248 if *editGo != "" && *editGo != "none" {
249 if !modfile.GoVersionRE.MatchString(*editGo) {
250 base.Fatalf(`go mod: invalid -go option; expecting something like "-go %s"`, gover.Local())
251 }
252 }
253 if *editToolchain != "" && *editToolchain != "none" {
254 if !modfile.ToolchainRE.MatchString(*editToolchain) {
255 base.Fatalf(`go mod: invalid -toolchain option; expecting something like "-toolchain go%s"`, gover.Local())
256 }
257 }
258
259 data, err := lockedfile.Read(gomod)
260 if err != nil {
261 base.Fatal(err)
262 }
263
264 modFile, err := modfile.Parse(gomod, data, nil)
265 if err != nil {
266 base.Fatalf("go: errors parsing %s:\n%s", base.ShortPath(gomod), err)
267 }
268
269 if *editModule != "" {
270 modFile.AddModuleStmt(*editModule)
271 }
272
273 if *editGo == "none" {
274 modFile.DropGoStmt()
275 } else if *editGo != "" {
276 if err := modFile.AddGoStmt(*editGo); err != nil {
277 base.Fatalf("go: internal error: %v", err)
278 }
279 }
280 if *editToolchain == "none" {
281 modFile.DropToolchainStmt()
282 } else if *editToolchain != "" {
283 if err := modFile.AddToolchainStmt(*editToolchain); err != nil {
284 base.Fatalf("go: internal error: %v", err)
285 }
286 }
287
288 if len(edits) > 0 {
289 for _, edit := range edits {
290 edit(modFile)
291 }
292 }
293 modFile.SortBlocks()
294 modFile.Cleanup()
295
296 if *editJSON {
297 editPrintJSON(modFile)
298 return
299 }
300
301 out, err := modFile.Format()
302 if err != nil {
303 base.Fatal(err)
304 }
305
306 if *editPrint {
307 os.Stdout.Write(out)
308 return
309 }
310
311
312
313 if unlock, err := modfetch.SideLock(ctx); err == nil {
314 defer unlock()
315 }
316
317 err = lockedfile.Transform(gomod, func(lockedData []byte) ([]byte, error) {
318 if !bytes.Equal(lockedData, data) {
319 return nil, errors.New("go.mod changed during editing; not overwriting")
320 }
321 return out, nil
322 })
323 if err != nil {
324 base.Fatal(err)
325 }
326 }
327
328
329 func parsePathVersion(flag, arg string) (path, version string) {
330 before, after, found := strings.Cut(arg, "@")
331 if !found {
332 base.Fatalf("go: -%s=%s: need path@version", flag, arg)
333 }
334 path, version = strings.TrimSpace(before), strings.TrimSpace(after)
335 if err := module.CheckImportPath(path); err != nil {
336 base.Fatalf("go: -%s=%s: invalid path: %v", flag, arg, err)
337 }
338
339 if !allowedVersionArg(version) {
340 base.Fatalf("go: -%s=%s: invalid version %q", flag, arg, version)
341 }
342
343 return path, version
344 }
345
346
347 func parsePath(flag, arg string) (path string) {
348 if strings.Contains(arg, "@") {
349 base.Fatalf("go: -%s=%s: need just path, not path@version", flag, arg)
350 }
351 path = arg
352 if err := module.CheckImportPath(path); err != nil {
353 base.Fatalf("go: -%s=%s: invalid path: %v", flag, arg, err)
354 }
355 return path
356 }
357
358
359
360 func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) {
361 if allowDirPath && modfile.IsDirectoryPath(arg) {
362 return arg, "", nil
363 }
364 before, after, found := strings.Cut(arg, "@")
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