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