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