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