Source file src/cmd/go/internal/generate/generate.go

Documentation: cmd/go/internal/generate

     1  // Copyright 2011 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  // Package generate implements the ``go generate'' command.
     6  package generate
     7  
     8  import (
     9  	"bufio"
    10  	"bytes"
    11  	"fmt"
    12  	"io"
    13  	"log"
    14  	"os"
    15  	"os/exec"
    16  	"path/filepath"
    17  	"regexp"
    18  	"strconv"
    19  	"strings"
    20  
    21  	"cmd/go/internal/base"
    22  	"cmd/go/internal/cfg"
    23  	"cmd/go/internal/load"
    24  	"cmd/go/internal/modload"
    25  	"cmd/go/internal/work"
    26  )
    27  
    28  var CmdGenerate = &base.Command{
    29  	Run:       runGenerate,
    30  	UsageLine: "go generate [-run regexp] [-n] [-v] [-x] [build flags] [file.go... | packages]",
    31  	Short:     "generate Go files by processing source",
    32  	Long: `
    33  Generate runs commands described by directives within existing
    34  files. Those commands can run any process but the intent is to
    35  create or update Go source files.
    36  
    37  Go generate is never run automatically by go build, go get, go test,
    38  and so on. It must be run explicitly.
    39  
    40  Go generate scans the file for directives, which are lines of
    41  the form,
    42  
    43  	//go:generate command argument...
    44  
    45  (note: no leading spaces and no space in "//go") where command
    46  is the generator to be run, corresponding to an executable file
    47  that can be run locally. It must either be in the shell path
    48  (gofmt), a fully qualified path (/usr/you/bin/mytool), or a
    49  command alias, described below.
    50  
    51  To convey to humans and machine tools that code is generated,
    52  generated source should have a line that matches the following
    53  regular expression (in Go syntax):
    54  
    55  	^// Code generated .* DO NOT EDIT\.$
    56  
    57  The line may appear anywhere in the file, but is typically
    58  placed near the beginning so it is easy to find.
    59  
    60  Note that go generate does not parse the file, so lines that look
    61  like directives in comments or multiline strings will be treated
    62  as directives.
    63  
    64  The arguments to the directive are space-separated tokens or
    65  double-quoted strings passed to the generator as individual
    66  arguments when it is run.
    67  
    68  Quoted strings use Go syntax and are evaluated before execution; a
    69  quoted string appears as a single argument to the generator.
    70  
    71  Go generate sets several variables when it runs the generator:
    72  
    73  	$GOARCH
    74  		The execution architecture (arm, amd64, etc.)
    75  	$GOOS
    76  		The execution operating system (linux, windows, etc.)
    77  	$GOFILE
    78  		The base name of the file.
    79  	$GOLINE
    80  		The line number of the directive in the source file.
    81  	$GOPACKAGE
    82  		The name of the package of the file containing the directive.
    83  	$DOLLAR
    84  		A dollar sign.
    85  
    86  Other than variable substitution and quoted-string evaluation, no
    87  special processing such as "globbing" is performed on the command
    88  line.
    89  
    90  As a last step before running the command, any invocations of any
    91  environment variables with alphanumeric names, such as $GOFILE or
    92  $HOME, are expanded throughout the command line. The syntax for
    93  variable expansion is $NAME on all operating systems. Due to the
    94  order of evaluation, variables are expanded even inside quoted
    95  strings. If the variable NAME is not set, $NAME expands to the
    96  empty string.
    97  
    98  A directive of the form,
    99  
   100  	//go:generate -command xxx args...
   101  
   102  specifies, for the remainder of this source file only, that the
   103  string xxx represents the command identified by the arguments. This
   104  can be used to create aliases or to handle multiword generators.
   105  For example,
   106  
   107  	//go:generate -command foo go tool foo
   108  
   109  specifies that the command "foo" represents the generator
   110  "go tool foo".
   111  
   112  Generate processes packages in the order given on the command line,
   113  one at a time. If the command line lists .go files from a single directory,
   114  they are treated as a single package. Within a package, generate processes the
   115  source files in a package in file name order, one at a time. Within
   116  a source file, generate runs generators in the order they appear
   117  in the file, one at a time. The go generate tool also sets the build
   118  tag "generate" so that files may be examined by go generate but ignored
   119  during build.
   120  
   121  If any generator returns an error exit status, "go generate" skips
   122  all further processing for that package.
   123  
   124  The generator is run in the package's source directory.
   125  
   126  Go generate accepts one specific flag:
   127  
   128  	-run=""
   129  		if non-empty, specifies a regular expression to select
   130  		directives whose full original source text (excluding
   131  		any trailing spaces and final newline) matches the
   132  		expression.
   133  
   134  It also accepts the standard build flags including -v, -n, and -x.
   135  The -v flag prints the names of packages and files as they are
   136  processed.
   137  The -n flag prints commands that would be executed.
   138  The -x flag prints commands as they are executed.
   139  
   140  For more about build flags, see 'go help build'.
   141  
   142  For more about specifying packages, see 'go help packages'.
   143  	`,
   144  }
   145  
   146  var (
   147  	generateRunFlag string         // generate -run flag
   148  	generateRunRE   *regexp.Regexp // compiled expression for -run
   149  )
   150  
   151  func init() {
   152  	work.AddBuildFlags(CmdGenerate)
   153  	CmdGenerate.Flag.StringVar(&generateRunFlag, "run", "", "")
   154  }
   155  
   156  func runGenerate(cmd *base.Command, args []string) {
   157  	load.IgnoreImports = true
   158  
   159  	if generateRunFlag != "" {
   160  		var err error
   161  		generateRunRE, err = regexp.Compile(generateRunFlag)
   162  		if err != nil {
   163  			log.Fatalf("generate: %s", err)
   164  		}
   165  	}
   166  
   167  	cfg.BuildContext.BuildTags = append(cfg.BuildContext.BuildTags, "generate")
   168  
   169  	// Even if the arguments are .go files, this loop suffices.
   170  	printed := false
   171  	for _, pkg := range load.Packages(args) {
   172  		if modload.Enabled() && pkg.Module != nil && !pkg.Module.Main {
   173  			if !printed {
   174  				fmt.Fprintf(os.Stderr, "go: not generating in packages in dependency modules\n")
   175  				printed = true
   176  			}
   177  			continue
   178  		}
   179  
   180  		pkgName := pkg.Name
   181  
   182  		for _, file := range pkg.InternalGoFiles() {
   183  			if !generate(pkgName, file) {
   184  				break
   185  			}
   186  		}
   187  
   188  		pkgName += "_test"
   189  
   190  		for _, file := range pkg.InternalXGoFiles() {
   191  			if !generate(pkgName, file) {
   192  				break
   193  			}
   194  		}
   195  	}
   196  }
   197  
   198  // generate runs the generation directives for a single file.
   199  func generate(pkg, absFile string) bool {
   200  	fd, err := os.Open(absFile)
   201  	if err != nil {
   202  		log.Fatalf("generate: %s", err)
   203  	}
   204  	defer fd.Close()
   205  	g := &Generator{
   206  		r:        fd,
   207  		path:     absFile,
   208  		pkg:      pkg,
   209  		commands: make(map[string][]string),
   210  	}
   211  	return g.run()
   212  }
   213  
   214  // A Generator represents the state of a single Go source file
   215  // being scanned for generator commands.
   216  type Generator struct {
   217  	r        io.Reader
   218  	path     string // full rooted path name.
   219  	dir      string // full rooted directory of file.
   220  	file     string // base name of file.
   221  	pkg      string
   222  	commands map[string][]string
   223  	lineNum  int // current line number.
   224  	env      []string
   225  }
   226  
   227  // run runs the generators in the current file.
   228  func (g *Generator) run() (ok bool) {
   229  	// Processing below here calls g.errorf on failure, which does panic(stop).
   230  	// If we encounter an error, we abort the package.
   231  	defer func() {
   232  		e := recover()
   233  		if e != nil {
   234  			ok = false
   235  			if e != stop {
   236  				panic(e)
   237  			}
   238  			base.SetExitStatus(1)
   239  		}
   240  	}()
   241  	g.dir, g.file = filepath.Split(g.path)
   242  	g.dir = filepath.Clean(g.dir) // No final separator please.
   243  	if cfg.BuildV {
   244  		fmt.Fprintf(os.Stderr, "%s\n", base.ShortPath(g.path))
   245  	}
   246  
   247  	// Scan for lines that start "//go:generate".
   248  	// Can't use bufio.Scanner because it can't handle long lines,
   249  	// which are likely to appear when using generate.
   250  	input := bufio.NewReader(g.r)
   251  	var err error
   252  	// One line per loop.
   253  	for {
   254  		g.lineNum++ // 1-indexed.
   255  		var buf []byte
   256  		buf, err = input.ReadSlice('\n')
   257  		if err == bufio.ErrBufferFull {
   258  			// Line too long - consume and ignore.
   259  			if isGoGenerate(buf) {
   260  				g.errorf("directive too long")
   261  			}
   262  			for err == bufio.ErrBufferFull {
   263  				_, err = input.ReadSlice('\n')
   264  			}
   265  			if err != nil {
   266  				break
   267  			}
   268  			continue
   269  		}
   270  
   271  		if err != nil {
   272  			// Check for marker at EOF without final \n.
   273  			if err == io.EOF && isGoGenerate(buf) {
   274  				err = io.ErrUnexpectedEOF
   275  			}
   276  			break
   277  		}
   278  
   279  		if !isGoGenerate(buf) {
   280  			continue
   281  		}
   282  		if generateRunFlag != "" {
   283  			if !generateRunRE.Match(bytes.TrimSpace(buf)) {
   284  				continue
   285  			}
   286  		}
   287  
   288  		g.setEnv()
   289  		words := g.split(string(buf))
   290  		if len(words) == 0 {
   291  			g.errorf("no arguments to directive")
   292  		}
   293  		if words[0] == "-command" {
   294  			g.setShorthand(words)
   295  			continue
   296  		}
   297  		// Run the command line.
   298  		if cfg.BuildN || cfg.BuildX {
   299  			fmt.Fprintf(os.Stderr, "%s\n", strings.Join(words, " "))
   300  		}
   301  		if cfg.BuildN {
   302  			continue
   303  		}
   304  		g.exec(words)
   305  	}
   306  	if err != nil && err != io.EOF {
   307  		g.errorf("error reading %s: %s", base.ShortPath(g.path), err)
   308  	}
   309  	return true
   310  }
   311  
   312  func isGoGenerate(buf []byte) bool {
   313  	return bytes.HasPrefix(buf, []byte("//go:generate ")) || bytes.HasPrefix(buf, []byte("//go:generate\t"))
   314  }
   315  
   316  // setEnv sets the extra environment variables used when executing a
   317  // single go:generate command.
   318  func (g *Generator) setEnv() {
   319  	g.env = []string{
   320  		"GOARCH=" + cfg.BuildContext.GOARCH,
   321  		"GOOS=" + cfg.BuildContext.GOOS,
   322  		"GOFILE=" + g.file,
   323  		"GOLINE=" + strconv.Itoa(g.lineNum),
   324  		"GOPACKAGE=" + g.pkg,
   325  		"DOLLAR=" + "$",
   326  	}
   327  }
   328  
   329  // split breaks the line into words, evaluating quoted
   330  // strings and evaluating environment variables.
   331  // The initial //go:generate element is present in line.
   332  func (g *Generator) split(line string) []string {
   333  	// Parse line, obeying quoted strings.
   334  	var words []string
   335  	line = line[len("//go:generate ") : len(line)-1] // Drop preamble and final newline.
   336  	// There may still be a carriage return.
   337  	if len(line) > 0 && line[len(line)-1] == '\r' {
   338  		line = line[:len(line)-1]
   339  	}
   340  	// One (possibly quoted) word per iteration.
   341  Words:
   342  	for {
   343  		line = strings.TrimLeft(line, " \t")
   344  		if len(line) == 0 {
   345  			break
   346  		}
   347  		if line[0] == '"' {
   348  			for i := 1; i < len(line); i++ {
   349  				c := line[i] // Only looking for ASCII so this is OK.
   350  				switch c {
   351  				case '\\':
   352  					if i+1 == len(line) {
   353  						g.errorf("bad backslash")
   354  					}
   355  					i++ // Absorb next byte (If it's a multibyte we'll get an error in Unquote).
   356  				case '"':
   357  					word, err := strconv.Unquote(line[0 : i+1])
   358  					if err != nil {
   359  						g.errorf("bad quoted string")
   360  					}
   361  					words = append(words, word)
   362  					line = line[i+1:]
   363  					// Check the next character is space or end of line.
   364  					if len(line) > 0 && line[0] != ' ' && line[0] != '\t' {
   365  						g.errorf("expect space after quoted argument")
   366  					}
   367  					continue Words
   368  				}
   369  			}
   370  			g.errorf("mismatched quoted string")
   371  		}
   372  		i := strings.IndexAny(line, " \t")
   373  		if i < 0 {
   374  			i = len(line)
   375  		}
   376  		words = append(words, line[0:i])
   377  		line = line[i:]
   378  	}
   379  	// Substitute command if required.
   380  	if len(words) > 0 && g.commands[words[0]] != nil {
   381  		// Replace 0th word by command substitution.
   382  		//
   383  		// Force a copy of the command definition to
   384  		// ensure words doesn't end up as a reference
   385  		// to the g.commands content.
   386  		tmpCmdWords := append([]string(nil), (g.commands[words[0]])...)
   387  		words = append(tmpCmdWords, words[1:]...)
   388  	}
   389  	// Substitute environment variables.
   390  	for i, word := range words {
   391  		words[i] = os.Expand(word, g.expandVar)
   392  	}
   393  	return words
   394  }
   395  
   396  var stop = fmt.Errorf("error in generation")
   397  
   398  // errorf logs an error message prefixed with the file and line number.
   399  // It then exits the program (with exit status 1) because generation stops
   400  // at the first error.
   401  func (g *Generator) errorf(format string, args ...interface{}) {
   402  	fmt.Fprintf(os.Stderr, "%s:%d: %s\n", base.ShortPath(g.path), g.lineNum,
   403  		fmt.Sprintf(format, args...))
   404  	panic(stop)
   405  }
   406  
   407  // expandVar expands the $XXX invocation in word. It is called
   408  // by os.Expand.
   409  func (g *Generator) expandVar(word string) string {
   410  	w := word + "="
   411  	for _, e := range g.env {
   412  		if strings.HasPrefix(e, w) {
   413  			return e[len(w):]
   414  		}
   415  	}
   416  	return os.Getenv(word)
   417  }
   418  
   419  // setShorthand installs a new shorthand as defined by a -command directive.
   420  func (g *Generator) setShorthand(words []string) {
   421  	// Create command shorthand.
   422  	if len(words) == 1 {
   423  		g.errorf("no command specified for -command")
   424  	}
   425  	command := words[1]
   426  	if g.commands[command] != nil {
   427  		g.errorf("command %q multiply defined", command)
   428  	}
   429  	g.commands[command] = words[2:len(words):len(words)] // force later append to make copy
   430  }
   431  
   432  // exec runs the command specified by the argument. The first word is
   433  // the command name itself.
   434  func (g *Generator) exec(words []string) {
   435  	cmd := exec.Command(words[0], words[1:]...)
   436  	// Standard in and out of generator should be the usual.
   437  	cmd.Stdout = os.Stdout
   438  	cmd.Stderr = os.Stderr
   439  	// Run the command in the package directory.
   440  	cmd.Dir = g.dir
   441  	cmd.Env = append(cfg.OrigEnv, g.env...)
   442  	err := cmd.Run()
   443  	if err != nil {
   444  		g.errorf("running %q: %s", words[0], err)
   445  	}
   446  }
   447  

View as plain text