...

Source file src/pkg/cmd/go/internal/modcmd/edit.go

     1	// Copyright 2018 The Go Authors. All rights reserved.
     2	// Use of this source code is governed by a BSD-style
     3	// license that can be found in the LICENSE file.
     4	
     5	// go mod edit
     6	
     7	package modcmd
     8	
     9	import (
    10		"bytes"
    11		"encoding/json"
    12		"fmt"
    13		"io/ioutil"
    14		"os"
    15		"path/filepath"
    16		"strings"
    17	
    18		"cmd/go/internal/base"
    19		"cmd/go/internal/modfetch"
    20		"cmd/go/internal/modfile"
    21		"cmd/go/internal/modload"
    22		"cmd/go/internal/module"
    23	)
    24	
    25	var cmdEdit = &base.Command{
    26		UsageLine: "go mod edit [editing flags] [go.mod]",
    27		Short:     "edit go.mod from tools or scripts",
    28		Long: `
    29	Edit provides a command-line interface for editing go.mod,
    30	for use primarily by tools or scripts. It reads only go.mod;
    31	it does not look up information about the modules involved.
    32	By default, edit reads and writes the go.mod file of the main module,
    33	but a different target file can be specified after the editing flags.
    34	
    35	The editing flags specify a sequence of editing operations.
    36	
    37	The -fmt flag reformats the go.mod file without making other changes.
    38	This reformatting is also implied by any other modifications that use or
    39	rewrite the go.mod file. The only time this flag is needed is if no other
    40	flags are specified, as in 'go mod edit -fmt'.
    41	
    42	The -module flag changes the module's path (the go.mod file's module line).
    43	
    44	The -require=path@version and -droprequire=path flags
    45	add and drop a requirement on the given module path and version.
    46	Note that -require overrides any existing requirements on path.
    47	These flags are mainly for tools that understand the module graph.
    48	Users should prefer 'go get path@version' or 'go get path@none',
    49	which make other go.mod adjustments as needed to satisfy
    50	constraints imposed by other modules.
    51	
    52	The -exclude=path@version and -dropexclude=path@version flags
    53	add and drop an exclusion for the given module path and version.
    54	Note that -exclude=path@version is a no-op if that exclusion already exists.
    55	
    56	The -replace=old[@v]=new[@v] and -dropreplace=old[@v] flags
    57	add and drop a replacement of the given module path and version pair.
    58	If the @v in old@v is omitted, the replacement applies to all versions
    59	with the old module path. If the @v in new@v is omitted, the new path
    60	should be a local module root directory, not a module path.
    61	Note that -replace overrides any existing replacements for old[@v].
    62	
    63	The -require, -droprequire, -exclude, -dropexclude, -replace,
    64	and -dropreplace editing flags may be repeated, and the changes
    65	are applied in the order given.
    66	
    67	The -go=version flag sets the expected Go language version.
    68	
    69	The -print flag prints the final go.mod in its text format instead of
    70	writing it back to go.mod.
    71	
    72	The -json flag prints the final go.mod file in JSON format instead of
    73	writing it back to go.mod. The JSON output corresponds to these Go types:
    74	
    75		type Module struct {
    76			Path string
    77			Version string
    78		}
    79	
    80		type GoMod struct {
    81			Module  Module
    82			Go      string
    83			Require []Require
    84			Exclude []Module
    85			Replace []Replace
    86		}
    87	
    88		type Require struct {
    89			Path string
    90			Version string
    91			Indirect bool
    92		}
    93	
    94		type Replace struct {
    95			Old Module
    96			New Module
    97		}
    98	
    99	Note that this only describes the go.mod file itself, not other modules
   100	referred to indirectly. For the full set of modules available to a build,
   101	use 'go list -m -json all'.
   102	
   103	For example, a tool can obtain the go.mod as a data structure by
   104	parsing the output of 'go mod edit -json' and can then make changes
   105	by invoking 'go mod edit' with -require, -exclude, and so on.
   106		`,
   107	}
   108	
   109	var (
   110		editFmt    = cmdEdit.Flag.Bool("fmt", false, "")
   111		editGo     = cmdEdit.Flag.String("go", "", "")
   112		editJSON   = cmdEdit.Flag.Bool("json", false, "")
   113		editPrint  = cmdEdit.Flag.Bool("print", false, "")
   114		editModule = cmdEdit.Flag.String("module", "", "")
   115		edits      []func(*modfile.File) // edits specified in flags
   116	)
   117	
   118	type flagFunc func(string)
   119	
   120	func (f flagFunc) String() string     { return "" }
   121	func (f flagFunc) Set(s string) error { f(s); return nil }
   122	
   123	func init() {
   124		cmdEdit.Run = runEdit // break init cycle
   125	
   126		cmdEdit.Flag.Var(flagFunc(flagRequire), "require", "")
   127		cmdEdit.Flag.Var(flagFunc(flagDropRequire), "droprequire", "")
   128		cmdEdit.Flag.Var(flagFunc(flagExclude), "exclude", "")
   129		cmdEdit.Flag.Var(flagFunc(flagDropReplace), "dropreplace", "")
   130		cmdEdit.Flag.Var(flagFunc(flagReplace), "replace", "")
   131		cmdEdit.Flag.Var(flagFunc(flagDropExclude), "dropexclude", "")
   132	
   133		base.AddBuildFlagsNX(&cmdEdit.Flag)
   134	}
   135	
   136	func runEdit(cmd *base.Command, args []string) {
   137		anyFlags :=
   138			*editModule != "" ||
   139				*editGo != "" ||
   140				*editJSON ||
   141				*editPrint ||
   142				*editFmt ||
   143				len(edits) > 0
   144	
   145		if !anyFlags {
   146			base.Fatalf("go mod edit: no flags specified (see 'go help mod edit').")
   147		}
   148	
   149		if *editJSON && *editPrint {
   150			base.Fatalf("go mod edit: cannot use both -json and -print")
   151		}
   152	
   153		if len(args) > 1 {
   154			base.Fatalf("go mod edit: too many arguments")
   155		}
   156		var gomod string
   157		if len(args) == 1 {
   158			gomod = args[0]
   159		} else {
   160			gomod = filepath.Join(modload.ModRoot(), "go.mod")
   161		}
   162	
   163		if *editModule != "" {
   164			if err := module.CheckPath(*editModule); err != nil {
   165				base.Fatalf("go mod: invalid -module: %v", err)
   166			}
   167		}
   168	
   169		if *editGo != "" {
   170			if !modfile.GoVersionRE.MatchString(*editGo) {
   171				base.Fatalf(`go mod: invalid -go option; expecting something like "-go 1.12"`)
   172			}
   173		}
   174	
   175		data, err := ioutil.ReadFile(gomod)
   176		if err != nil {
   177			base.Fatalf("go: %v", err)
   178		}
   179	
   180		modFile, err := modfile.Parse(gomod, data, nil)
   181		if err != nil {
   182			base.Fatalf("go: errors parsing %s:\n%s", base.ShortPath(gomod), err)
   183		}
   184	
   185		if *editModule != "" {
   186			modFile.AddModuleStmt(*editModule)
   187		}
   188	
   189		if *editGo != "" {
   190			if err := modFile.AddGoStmt(*editGo); err != nil {
   191				base.Fatalf("go: internal error: %v", err)
   192			}
   193		}
   194	
   195		if len(edits) > 0 {
   196			for _, edit := range edits {
   197				edit(modFile)
   198			}
   199		}
   200		modFile.SortBlocks()
   201		modFile.Cleanup() // clean file after edits
   202	
   203		if *editJSON {
   204			editPrintJSON(modFile)
   205			return
   206		}
   207	
   208		out, err := modFile.Format()
   209		if err != nil {
   210			base.Fatalf("go: %v", err)
   211		}
   212	
   213		if *editPrint {
   214			os.Stdout.Write(out)
   215			return
   216		}
   217	
   218		unlock := modfetch.SideLock()
   219		defer unlock()
   220		lockedData, err := ioutil.ReadFile(gomod)
   221		if err == nil && !bytes.Equal(lockedData, data) {
   222			base.Fatalf("go: go.mod changed during editing; not overwriting")
   223		}
   224		if err := ioutil.WriteFile(gomod, out, 0666); err != nil {
   225			base.Fatalf("go: %v", err)
   226		}
   227	}
   228	
   229	// parsePathVersion parses -flag=arg expecting arg to be path@version.
   230	func parsePathVersion(flag, arg string) (path, version string) {
   231		i := strings.Index(arg, "@")
   232		if i < 0 {
   233			base.Fatalf("go mod: -%s=%s: need path@version", flag, arg)
   234		}
   235		path, version = strings.TrimSpace(arg[:i]), strings.TrimSpace(arg[i+1:])
   236		if err := module.CheckPath(path); err != nil {
   237			base.Fatalf("go mod: -%s=%s: invalid path: %v", flag, arg, err)
   238		}
   239	
   240		// We don't call modfile.CheckPathVersion, because that insists
   241		// on versions being in semver form, but here we want to allow
   242		// versions like "master" or "1234abcdef", which the go command will resolve
   243		// the next time it runs (or during -fix).
   244		// Even so, we need to make sure the version is a valid token.
   245		if modfile.MustQuote(version) {
   246			base.Fatalf("go mod: -%s=%s: invalid version %q", flag, arg, version)
   247		}
   248	
   249		return path, version
   250	}
   251	
   252	// parsePath parses -flag=arg expecting arg to be path (not path@version).
   253	func parsePath(flag, arg string) (path string) {
   254		if strings.Contains(arg, "@") {
   255			base.Fatalf("go mod: -%s=%s: need just path, not path@version", flag, arg)
   256		}
   257		path = arg
   258		if err := module.CheckPath(path); err != nil {
   259			base.Fatalf("go mod: -%s=%s: invalid path: %v", flag, arg, err)
   260		}
   261		return path
   262	}
   263	
   264	// parsePathVersionOptional parses path[@version], using adj to
   265	// describe any errors.
   266	func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) {
   267		if i := strings.Index(arg, "@"); i < 0 {
   268			path = arg
   269		} else {
   270			path, version = strings.TrimSpace(arg[:i]), strings.TrimSpace(arg[i+1:])
   271		}
   272		if err := module.CheckPath(path); err != nil {
   273			if !allowDirPath || !modfile.IsDirectoryPath(path) {
   274				return path, version, fmt.Errorf("invalid %s path: %v", adj, err)
   275			}
   276		}
   277		if path != arg && modfile.MustQuote(version) {
   278			return path, version, fmt.Errorf("invalid %s version: %q", adj, version)
   279		}
   280		return path, version, nil
   281	}
   282	
   283	// flagRequire implements the -require flag.
   284	func flagRequire(arg string) {
   285		path, version := parsePathVersion("require", arg)
   286		edits = append(edits, func(f *modfile.File) {
   287			if err := f.AddRequire(path, version); err != nil {
   288				base.Fatalf("go mod: -require=%s: %v", arg, err)
   289			}
   290		})
   291	}
   292	
   293	// flagDropRequire implements the -droprequire flag.
   294	func flagDropRequire(arg string) {
   295		path := parsePath("droprequire", arg)
   296		edits = append(edits, func(f *modfile.File) {
   297			if err := f.DropRequire(path); err != nil {
   298				base.Fatalf("go mod: -droprequire=%s: %v", arg, err)
   299			}
   300		})
   301	}
   302	
   303	// flagExclude implements the -exclude flag.
   304	func flagExclude(arg string) {
   305		path, version := parsePathVersion("exclude", arg)
   306		edits = append(edits, func(f *modfile.File) {
   307			if err := f.AddExclude(path, version); err != nil {
   308				base.Fatalf("go mod: -exclude=%s: %v", arg, err)
   309			}
   310		})
   311	}
   312	
   313	// flagDropExclude implements the -dropexclude flag.
   314	func flagDropExclude(arg string) {
   315		path, version := parsePathVersion("dropexclude", arg)
   316		edits = append(edits, func(f *modfile.File) {
   317			if err := f.DropExclude(path, version); err != nil {
   318				base.Fatalf("go mod: -dropexclude=%s: %v", arg, err)
   319			}
   320		})
   321	}
   322	
   323	// flagReplace implements the -replace flag.
   324	func flagReplace(arg string) {
   325		var i int
   326		if i = strings.Index(arg, "="); i < 0 {
   327			base.Fatalf("go mod: -replace=%s: need old[@v]=new[@w] (missing =)", arg)
   328		}
   329		old, new := strings.TrimSpace(arg[:i]), strings.TrimSpace(arg[i+1:])
   330		if strings.HasPrefix(new, ">") {
   331			base.Fatalf("go mod: -replace=%s: separator between old and new is =, not =>", arg)
   332		}
   333		oldPath, oldVersion, err := parsePathVersionOptional("old", old, false)
   334		if err != nil {
   335			base.Fatalf("go mod: -replace=%s: %v", arg, err)
   336		}
   337		newPath, newVersion, err := parsePathVersionOptional("new", new, true)
   338		if err != nil {
   339			base.Fatalf("go mod: -replace=%s: %v", arg, err)
   340		}
   341		if newPath == new && !modfile.IsDirectoryPath(new) {
   342			base.Fatalf("go mod: -replace=%s: unversioned new path must be local directory", arg)
   343		}
   344	
   345		edits = append(edits, func(f *modfile.File) {
   346			if err := f.AddReplace(oldPath, oldVersion, newPath, newVersion); err != nil {
   347				base.Fatalf("go mod: -replace=%s: %v", arg, err)
   348			}
   349		})
   350	}
   351	
   352	// flagDropReplace implements the -dropreplace flag.
   353	func flagDropReplace(arg string) {
   354		path, version, err := parsePathVersionOptional("old", arg, true)
   355		if err != nil {
   356			base.Fatalf("go mod: -dropreplace=%s: %v", arg, err)
   357		}
   358		edits = append(edits, func(f *modfile.File) {
   359			if err := f.DropReplace(path, version); err != nil {
   360				base.Fatalf("go mod: -dropreplace=%s: %v", arg, err)
   361			}
   362		})
   363	}
   364	
   365	// fileJSON is the -json output data structure.
   366	type fileJSON struct {
   367		Module  module.Version
   368		Go      string `json:",omitempty"`
   369		Require []requireJSON
   370		Exclude []module.Version
   371		Replace []replaceJSON
   372	}
   373	
   374	type requireJSON struct {
   375		Path     string
   376		Version  string `json:",omitempty"`
   377		Indirect bool   `json:",omitempty"`
   378	}
   379	
   380	type replaceJSON struct {
   381		Old module.Version
   382		New module.Version
   383	}
   384	
   385	// editPrintJSON prints the -json output.
   386	func editPrintJSON(modFile *modfile.File) {
   387		var f fileJSON
   388		if modFile.Module != nil {
   389			f.Module = modFile.Module.Mod
   390		}
   391		if modFile.Go != nil {
   392			f.Go = modFile.Go.Version
   393		}
   394		for _, r := range modFile.Require {
   395			f.Require = append(f.Require, requireJSON{Path: r.Mod.Path, Version: r.Mod.Version, Indirect: r.Indirect})
   396		}
   397		for _, x := range modFile.Exclude {
   398			f.Exclude = append(f.Exclude, x.Mod)
   399		}
   400		for _, r := range modFile.Replace {
   401			f.Replace = append(f.Replace, replaceJSON{r.Old, r.New})
   402		}
   403		data, err := json.MarshalIndent(&f, "", "\t")
   404		if err != nil {
   405			base.Fatalf("go: internal error: %v", err)
   406		}
   407		data = append(data, '\n')
   408		os.Stdout.Write(data)
   409	}
   410	

View as plain text