github.com/mdempsky/go@v0.0.0-20151201204031-5dd372bd1e70/src/cmd/go/generate.go (about)

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