Source file src/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  	"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) // edits specified in flags
   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 // break init cycle
   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() // clean file after edits
   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  	// Make a best-effort attempt to acquire the side lock, only to exclude
   296  	// previous versions of the 'go' command from making simultaneous edits.
   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  // parsePathVersion parses -flag=arg expecting arg to be path@version.
   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  // parsePath parses -flag=arg expecting arg to be path (not path@version).
   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  // parsePathVersionOptional parses path[@version], using adj to
   343  // describe any errors.
   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  // parseVersionInterval parses a single version like "v1.2.3" or a closed
   364  // interval like "[v1.2.3,v1.4.5]". Note that a single version has the same
   365  // representation as an interval with equal upper and lower bounds: both
   366  // Low and High are set.
   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  // allowedVersionArg returns whether a token may be used as a version in go.mod.
   391  // We don't call modfile.CheckPathVersion, because that insists on versions
   392  // being in semver form, but here we want to allow versions like "master" or
   393  // "1234abcdef", which the go command will resolve the next time it runs (or
   394  // during -fix).  Even so, we need to make sure the version is a valid token.
   395  func allowedVersionArg(arg string) bool {
   396  	return !modfile.MustQuote(arg)
   397  }
   398  
   399  // flagGodebug implements the -godebug flag.
   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  // flagDropGodebug implements the -dropgodebug flag.
   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  // flagRequire implements the -require flag.
   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  // flagDropRequire implements the -droprequire flag.
   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  // flagExclude implements the -exclude flag.
   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  // flagDropExclude implements the -dropexclude flag.
   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  // flagReplace implements the -replace flag.
   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  // flagDropReplace implements the -dropreplace flag.
   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  // flagRetract implements the -retract flag.
   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  // flagDropRetract implements the -dropretract flag.
   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  // flagTool implements the -tool flag.
   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  // flagDropTool implements the -droptool flag.
   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  // fileJSON is the -json output data structure.
   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  // editPrintJSON prints the -json output.
   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