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