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