1
2
3
4
5
6 package generate
7
8 import (
9 "bufio"
10 "bytes"
11 "context"
12 "fmt"
13 "go/parser"
14 "go/token"
15 "io"
16 "log"
17 "os"
18 "os/exec"
19 "path/filepath"
20 "regexp"
21 "slices"
22 "strconv"
23 "strings"
24
25 "cmd/go/internal/base"
26 "cmd/go/internal/cfg"
27 "cmd/go/internal/load"
28 "cmd/go/internal/modload"
29 "cmd/go/internal/str"
30 "cmd/go/internal/work"
31 "cmd/internal/pathcache"
32 )
33
34 var CmdGenerate = &base.Command{
35 Run: runGenerate,
36 UsageLine: "go generate [-run regexp] [-n] [-v] [-x] [build flags] [file.go... | packages]",
37 Short: "generate Go files by processing source",
38 Long: `
39 Generate runs commands described by directives within existing
40 files. Those commands can run any process but the intent is to
41 create or update Go source files.
42
43 Go generate is never run automatically by go build, go test,
44 and so on. It must be run explicitly.
45
46 Go generate scans the file for directives, which are lines of
47 the form,
48
49 //go:generate command argument...
50
51 (note: no leading spaces and no space in "//go") where command
52 is the generator to be run, corresponding to an executable file
53 that can be run locally. It must either be in the shell path
54 (gofmt), a fully qualified path (/usr/you/bin/mytool), or a
55 command alias, described below.
56
57 Note that go generate does not parse the file, so lines that look
58 like directives in comments or multiline strings will be treated
59 as directives.
60
61 The arguments to the directive are space-separated tokens or
62 double-quoted strings passed to the generator as individual
63 arguments when it is run.
64
65 Quoted strings use Go syntax and are evaluated before execution; a
66 quoted string appears as a single argument to the generator.
67
68 To convey to humans and machine tools that code is generated,
69 generated source should have a line that matches the following
70 regular expression (in Go syntax):
71
72 ^// Code generated .* DO NOT EDIT\.$
73
74 This line must appear before the first non-comment, non-blank
75 text in the file.
76
77 Go generate sets several variables when it runs the generator:
78
79 $GOARCH
80 The execution architecture (arm, amd64, etc.)
81 $GOOS
82 The execution operating system (linux, windows, etc.)
83 $GOFILE
84 The base name of the file.
85 $GOLINE
86 The line number of the directive in the source file.
87 $GOPACKAGE
88 The name of the package of the file containing the directive.
89 $GOROOT
90 The GOROOT directory for the 'go' command that invoked the
91 generator, containing the Go toolchain and standard library.
92 $DOLLAR
93 A dollar sign.
94 $PATH
95 The $PATH of the parent process, with $GOROOT/bin
96 placed at the beginning. This causes generators
97 that execute 'go' commands to use the same 'go'
98 as the parent 'go generate' command.
99
100 Other than variable substitution and quoted-string evaluation, no
101 special processing such as "globbing" is performed on the command
102 line.
103
104 As a last step before running the command, any invocations of any
105 environment variables with alphanumeric names, such as $GOFILE or
106 $HOME, are expanded throughout the command line. The syntax for
107 variable expansion is $NAME on all operating systems. Due to the
108 order of evaluation, variables are expanded even inside quoted
109 strings. If the variable NAME is not set, $NAME expands to the
110 empty string.
111
112 A directive of the form,
113
114 //go:generate -command xxx args...
115
116 specifies, for the remainder of this source file only, that the
117 string xxx represents the command identified by the arguments. This
118 can be used to create aliases or to handle multiword generators.
119 For example,
120
121 //go:generate -command foo go tool foo
122
123 specifies that the command "foo" represents the generator
124 "go tool foo".
125
126 Generate processes packages in the order given on the command line,
127 one at a time. If the command line lists .go files from a single directory,
128 they are treated as a single package. Within a package, generate processes the
129 source files in a package in file name order, one at a time. Within
130 a source file, generate runs generators in the order they appear
131 in the file, one at a time. The go generate tool also sets the build
132 tag "generate" so that files may be examined by go generate but ignored
133 during build.
134
135 For packages with invalid code, generate processes only source files with a
136 valid package clause.
137
138 If any generator returns an error exit status, "go generate" skips
139 all further processing for that package.
140
141 The generator is run in the package's source directory.
142
143 Go generate accepts two specific flags:
144
145 -run=""
146 if non-empty, specifies a regular expression to select
147 directives whose full original source text (excluding
148 any trailing spaces and final newline) matches the
149 expression.
150
151 -skip=""
152 if non-empty, specifies a regular expression to suppress
153 directives whose full original source text (excluding
154 any trailing spaces and final newline) matches the
155 expression. If a directive matches both the -run and
156 the -skip arguments, it is skipped.
157
158 It also accepts the standard build flags including -v, -n, and -x.
159 The -v flag prints the names of packages and files as they are
160 processed.
161 The -n flag prints commands that would be executed.
162 The -x flag prints commands as they are executed.
163
164 For more about build flags, see 'go help build'.
165
166 For more about specifying packages, see 'go help packages'.
167 `,
168 }
169
170 var (
171 generateRunFlag string
172 generateRunRE *regexp.Regexp
173
174 generateSkipFlag string
175 generateSkipRE *regexp.Regexp
176 )
177
178 func init() {
179 work.AddBuildFlags(CmdGenerate, work.DefaultBuildFlags)
180 CmdGenerate.Flag.StringVar(&generateRunFlag, "run", "", "")
181 CmdGenerate.Flag.StringVar(&generateSkipFlag, "skip", "", "")
182 }
183
184 func runGenerate(ctx context.Context, cmd *base.Command, args []string) {
185 modload.InitWorkfile()
186
187 if generateRunFlag != "" {
188 var err error
189 generateRunRE, err = regexp.Compile(generateRunFlag)
190 if err != nil {
191 log.Fatalf("generate: %s", err)
192 }
193 }
194 if generateSkipFlag != "" {
195 var err error
196 generateSkipRE, err = regexp.Compile(generateSkipFlag)
197 if err != nil {
198 log.Fatalf("generate: %s", err)
199 }
200 }
201
202 cfg.BuildContext.BuildTags = append(cfg.BuildContext.BuildTags, "generate")
203
204
205 printed := false
206 pkgOpts := load.PackageOpts{IgnoreImports: true}
207 for _, pkg := range load.PackagesAndErrors(ctx, pkgOpts, args) {
208 if modload.Enabled() && pkg.Module != nil && !pkg.Module.Main {
209 if !printed {
210 fmt.Fprintf(os.Stderr, "go: not generating in packages in dependency modules\n")
211 printed = true
212 }
213 continue
214 }
215
216 if pkg.Error != nil && len(pkg.InternalAllGoFiles()) == 0 {
217
218
219
220 base.Errorf("%v", pkg.Error)
221 }
222
223 for _, file := range pkg.InternalGoFiles() {
224 if !generate(file) {
225 break
226 }
227 }
228
229 for _, file := range pkg.InternalXGoFiles() {
230 if !generate(file) {
231 break
232 }
233 }
234 }
235 base.ExitIfErrors()
236 }
237
238
239 func generate(absFile string) bool {
240 src, err := os.ReadFile(absFile)
241 if err != nil {
242 log.Fatalf("generate: %s", err)
243 }
244
245
246 filePkg, err := parser.ParseFile(token.NewFileSet(), "", src, parser.PackageClauseOnly)
247 if err != nil {
248
249 return true
250 }
251
252 g := &Generator{
253 r: bytes.NewReader(src),
254 path: absFile,
255 pkg: filePkg.Name.String(),
256 commands: make(map[string][]string),
257 }
258 return g.run()
259 }
260
261
262
263 type Generator struct {
264 r io.Reader
265 path string
266 dir string
267 file string
268 pkg string
269 commands map[string][]string
270 lineNum int
271 env []string
272 }
273
274
275 func (g *Generator) run() (ok bool) {
276
277
278 defer func() {
279 e := recover()
280 if e != nil {
281 ok = false
282 if e != stop {
283 panic(e)
284 }
285 base.SetExitStatus(1)
286 }
287 }()
288 g.dir, g.file = filepath.Split(g.path)
289 g.dir = filepath.Clean(g.dir)
290 if cfg.BuildV {
291 fmt.Fprintf(os.Stderr, "%s\n", base.ShortPath(g.path))
292 }
293
294
295
296
297 input := bufio.NewReader(g.r)
298 var err error
299
300 for {
301 g.lineNum++
302 var buf []byte
303 buf, err = input.ReadSlice('\n')
304 if err == bufio.ErrBufferFull {
305
306 if isGoGenerate(buf) {
307 g.errorf("directive too long")
308 }
309 for err == bufio.ErrBufferFull {
310 _, err = input.ReadSlice('\n')
311 }
312 if err != nil {
313 break
314 }
315 continue
316 }
317
318 if err != nil {
319
320 if err == io.EOF && isGoGenerate(buf) {
321 err = io.ErrUnexpectedEOF
322 }
323 break
324 }
325
326 if !isGoGenerate(buf) {
327 continue
328 }
329 if generateRunFlag != "" && !generateRunRE.Match(bytes.TrimSpace(buf)) {
330 continue
331 }
332 if generateSkipFlag != "" && generateSkipRE.Match(bytes.TrimSpace(buf)) {
333 continue
334 }
335
336 g.setEnv()
337 words := g.split(string(buf))
338 if len(words) == 0 {
339 g.errorf("no arguments to directive")
340 }
341 if words[0] == "-command" {
342 g.setShorthand(words)
343 continue
344 }
345
346 if cfg.BuildN || cfg.BuildX {
347 fmt.Fprintf(os.Stderr, "%s\n", strings.Join(words, " "))
348 }
349 if cfg.BuildN {
350 continue
351 }
352 g.exec(words)
353 }
354 if err != nil && err != io.EOF {
355 g.errorf("error reading %s: %s", base.ShortPath(g.path), err)
356 }
357 return true
358 }
359
360 func isGoGenerate(buf []byte) bool {
361 return bytes.HasPrefix(buf, []byte("//go:generate ")) || bytes.HasPrefix(buf, []byte("//go:generate\t"))
362 }
363
364
365
366 func (g *Generator) setEnv() {
367 env := []string{
368 "GOROOT=" + cfg.GOROOT,
369 "GOARCH=" + cfg.BuildContext.GOARCH,
370 "GOOS=" + cfg.BuildContext.GOOS,
371 "GOFILE=" + g.file,
372 "GOLINE=" + strconv.Itoa(g.lineNum),
373 "GOPACKAGE=" + g.pkg,
374 "DOLLAR=" + "$",
375 }
376 env = base.AppendPATH(env)
377 env = base.AppendPWD(env, g.dir)
378 g.env = env
379 }
380
381
382
383
384 func (g *Generator) split(line string) []string {
385
386 var words []string
387 line = line[len("//go:generate ") : len(line)-1]
388
389 if len(line) > 0 && line[len(line)-1] == '\r' {
390 line = line[:len(line)-1]
391 }
392
393 Words:
394 for {
395 line = strings.TrimLeft(line, " \t")
396 if len(line) == 0 {
397 break
398 }
399 if line[0] == '"' {
400 for i := 1; i < len(line); i++ {
401 c := line[i]
402 switch c {
403 case '\\':
404 if i+1 == len(line) {
405 g.errorf("bad backslash")
406 }
407 i++
408 case '"':
409 word, err := strconv.Unquote(line[0 : i+1])
410 if err != nil {
411 g.errorf("bad quoted string")
412 }
413 words = append(words, word)
414 line = line[i+1:]
415
416 if len(line) > 0 && line[0] != ' ' && line[0] != '\t' {
417 g.errorf("expect space after quoted argument")
418 }
419 continue Words
420 }
421 }
422 g.errorf("mismatched quoted string")
423 }
424 i := strings.IndexAny(line, " \t")
425 if i < 0 {
426 i = len(line)
427 }
428 words = append(words, line[0:i])
429 line = line[i:]
430 }
431
432 if len(words) > 0 && g.commands[words[0]] != nil {
433
434
435
436
437
438 tmpCmdWords := append([]string(nil), (g.commands[words[0]])...)
439 words = append(tmpCmdWords, words[1:]...)
440 }
441
442 for i, word := range words {
443 words[i] = os.Expand(word, g.expandVar)
444 }
445 return words
446 }
447
448 var stop = fmt.Errorf("error in generation")
449
450
451
452
453 func (g *Generator) errorf(format string, args ...any) {
454 fmt.Fprintf(os.Stderr, "%s:%d: %s\n", base.ShortPath(g.path), g.lineNum,
455 fmt.Sprintf(format, args...))
456 panic(stop)
457 }
458
459
460
461 func (g *Generator) expandVar(word string) string {
462 w := word + "="
463 for _, e := range g.env {
464 if strings.HasPrefix(e, w) {
465 return e[len(w):]
466 }
467 }
468 return os.Getenv(word)
469 }
470
471
472 func (g *Generator) setShorthand(words []string) {
473
474 if len(words) == 1 {
475 g.errorf("no command specified for -command")
476 }
477 command := words[1]
478 if g.commands[command] != nil {
479 g.errorf("command %q multiply defined", command)
480 }
481 g.commands[command] = slices.Clip(words[2:])
482 }
483
484
485
486 func (g *Generator) exec(words []string) {
487 path := words[0]
488 if path != "" && !strings.Contains(path, string(os.PathSeparator)) {
489
490
491
492
493 gorootBinPath, err := pathcache.LookPath(filepath.Join(cfg.GOROOTbin, path))
494 if err == nil {
495 path = gorootBinPath
496 }
497 }
498 cmd := exec.Command(path, words[1:]...)
499 cmd.Args[0] = words[0]
500
501
502 cmd.Stdout = os.Stdout
503 cmd.Stderr = os.Stderr
504
505 cmd.Dir = g.dir
506 cmd.Env = str.StringList(cfg.OrigEnv, g.env)
507 err := cmd.Run()
508 if err != nil {
509 g.errorf("running %q: %s", words[0], err)
510 }
511 }
512
View as plain text