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