github.com/q45/go@v0.0.0-20151101211701-a4fb8c13db3f/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 -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  }
   183  
   184  // run runs the generators in the current file.
   185  func (g *Generator) run() (ok bool) {
   186  	// Processing below here calls g.errorf on failure, which does panic(stop).
   187  	// If we encounter an error, we abort the package.
   188  	defer func() {
   189  		e := recover()
   190  		if e != nil {
   191  			ok = false
   192  			if e != stop {
   193  				panic(e)
   194  			}
   195  			setExitStatus(1)
   196  		}
   197  	}()
   198  	g.dir, g.file = filepath.Split(g.path)
   199  	g.dir = filepath.Clean(g.dir) // No final separator please.
   200  	if buildV {
   201  		fmt.Fprintf(os.Stderr, "%s\n", shortPath(g.path))
   202  	}
   203  
   204  	// Scan for lines that start "//go:generate".
   205  	// Can't use bufio.Scanner because it can't handle long lines,
   206  	// which are likely to appear when using generate.
   207  	input := bufio.NewReader(g.r)
   208  	var err error
   209  	// One line per loop.
   210  	for {
   211  		g.lineNum++ // 1-indexed.
   212  		var buf []byte
   213  		buf, err = input.ReadSlice('\n')
   214  		if err == bufio.ErrBufferFull {
   215  			// Line too long - consume and ignore.
   216  			if isGoGenerate(buf) {
   217  				g.errorf("directive too long")
   218  			}
   219  			for err == bufio.ErrBufferFull {
   220  				_, err = input.ReadSlice('\n')
   221  			}
   222  			if err != nil {
   223  				break
   224  			}
   225  			continue
   226  		}
   227  
   228  		if err != nil {
   229  			// Check for marker at EOF without final \n.
   230  			if err == io.EOF && isGoGenerate(buf) {
   231  				err = io.ErrUnexpectedEOF
   232  			}
   233  			break
   234  		}
   235  
   236  		if !isGoGenerate(buf) {
   237  			continue
   238  		}
   239  		if generateRunFlag != "" {
   240  			if !generateRunRE.Match(bytes.TrimSpace(buf)) {
   241  				continue
   242  			}
   243  		}
   244  
   245  		words := g.split(string(buf))
   246  		if len(words) == 0 {
   247  			g.errorf("no arguments to directive")
   248  		}
   249  		if words[0] == "-command" {
   250  			g.setShorthand(words)
   251  			continue
   252  		}
   253  		// Run the command line.
   254  		if buildN || buildX {
   255  			fmt.Fprintf(os.Stderr, "%s\n", strings.Join(words, " "))
   256  		}
   257  		if buildN {
   258  			continue
   259  		}
   260  		g.exec(words)
   261  	}
   262  	if err != nil && err != io.EOF {
   263  		g.errorf("error reading %s: %s", shortPath(g.path), err)
   264  	}
   265  	return true
   266  }
   267  
   268  func isGoGenerate(buf []byte) bool {
   269  	return bytes.HasPrefix(buf, []byte("//go:generate ")) || bytes.HasPrefix(buf, []byte("//go:generate\t"))
   270  }
   271  
   272  // split breaks the line into words, evaluating quoted
   273  // strings and evaluating environment variables.
   274  // The initial //go:generate element is present in line.
   275  func (g *Generator) split(line string) []string {
   276  	// Parse line, obeying quoted strings.
   277  	var words []string
   278  	line = line[len("//go:generate ") : len(line)-1] // Drop preamble and final newline.
   279  	// There may still be a carriage return.
   280  	if len(line) > 0 && line[len(line)-1] == '\r' {
   281  		line = line[:len(line)-1]
   282  	}
   283  	// One (possibly quoted) word per iteration.
   284  Words:
   285  	for {
   286  		line = strings.TrimLeft(line, " \t")
   287  		if len(line) == 0 {
   288  			break
   289  		}
   290  		if line[0] == '"' {
   291  			for i := 1; i < len(line); i++ {
   292  				c := line[i] // Only looking for ASCII so this is OK.
   293  				switch c {
   294  				case '\\':
   295  					if i+1 == len(line) {
   296  						g.errorf("bad backslash")
   297  					}
   298  					i++ // Absorb next byte (If it's a multibyte we'll get an error in Unquote).
   299  				case '"':
   300  					word, err := strconv.Unquote(line[0 : i+1])
   301  					if err != nil {
   302  						g.errorf("bad quoted string")
   303  					}
   304  					words = append(words, word)
   305  					line = line[i+1:]
   306  					// Check the next character is space or end of line.
   307  					if len(line) > 0 && line[0] != ' ' && line[0] != '\t' {
   308  						g.errorf("expect space after quoted argument")
   309  					}
   310  					continue Words
   311  				}
   312  			}
   313  			g.errorf("mismatched quoted string")
   314  		}
   315  		i := strings.IndexAny(line, " \t")
   316  		if i < 0 {
   317  			i = len(line)
   318  		}
   319  		words = append(words, line[0:i])
   320  		line = line[i:]
   321  	}
   322  	// Substitute command if required.
   323  	if len(words) > 0 && g.commands[words[0]] != nil {
   324  		// Replace 0th word by command substitution.
   325  		words = append(g.commands[words[0]], words[1:]...)
   326  	}
   327  	// Substitute environment variables.
   328  	for i, word := range words {
   329  		words[i] = os.Expand(word, g.expandVar)
   330  	}
   331  	return words
   332  }
   333  
   334  var stop = fmt.Errorf("error in generation")
   335  
   336  // errorf logs an error message prefixed with the file and line number.
   337  // It then exits the program (with exit status 1) because generation stops
   338  // at the first error.
   339  func (g *Generator) errorf(format string, args ...interface{}) {
   340  	fmt.Fprintf(os.Stderr, "%s:%d: %s\n", shortPath(g.path), g.lineNum,
   341  		fmt.Sprintf(format, args...))
   342  	panic(stop)
   343  }
   344  
   345  // expandVar expands the $XXX invocation in word. It is called
   346  // by os.Expand.
   347  func (g *Generator) expandVar(word string) string {
   348  	switch word {
   349  	case "GOARCH":
   350  		return buildContext.GOARCH
   351  	case "GOOS":
   352  		return buildContext.GOOS
   353  	case "GOFILE":
   354  		return g.file
   355  	case "GOLINE":
   356  		return fmt.Sprint(g.lineNum)
   357  	case "GOPACKAGE":
   358  		return g.pkg
   359  	case "DOLLAR":
   360  		return "$"
   361  	default:
   362  		return os.Getenv(word)
   363  	}
   364  }
   365  
   366  // identLength returns the length of the identifier beginning the string.
   367  func (g *Generator) identLength(word string) int {
   368  	for i, r := range word {
   369  		if r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r) {
   370  			continue
   371  		}
   372  		return i
   373  	}
   374  	return len(word)
   375  }
   376  
   377  // setShorthand installs a new shorthand as defined by a -command directive.
   378  func (g *Generator) setShorthand(words []string) {
   379  	// Create command shorthand.
   380  	if len(words) == 1 {
   381  		g.errorf("no command specified for -command")
   382  	}
   383  	command := words[1]
   384  	if g.commands[command] != nil {
   385  		g.errorf("command %q defined multiply defined", command)
   386  	}
   387  	g.commands[command] = words[2:len(words):len(words)] // force later append to make copy
   388  }
   389  
   390  // exec runs the command specified by the argument. The first word is
   391  // the command name itself.
   392  func (g *Generator) exec(words []string) {
   393  	cmd := exec.Command(words[0], words[1:]...)
   394  	// Standard in and out of generator should be the usual.
   395  	cmd.Stdout = os.Stdout
   396  	cmd.Stderr = os.Stderr
   397  	// Run the command in the package directory.
   398  	cmd.Dir = g.dir
   399  	env := []string{
   400  		"GOARCH=" + runtime.GOARCH,
   401  		"GOOS=" + runtime.GOOS,
   402  		"GOFILE=" + g.file,
   403  		"GOPACKAGE=" + g.pkg,
   404  	}
   405  	cmd.Env = mergeEnvLists(env, origEnv)
   406  	err := cmd.Run()
   407  	if err != nil {
   408  		g.errorf("running %q: %s", words[0], err)
   409  	}
   410  }