Source file
src/cmd/doc/main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 package main
44
45 import (
46 "bytes"
47 "context"
48 "errors"
49 "flag"
50 "fmt"
51 "go/build"
52 "go/token"
53 "io"
54 "log"
55 "net"
56 "net/http"
57 "os"
58 "os/exec"
59 "os/signal"
60 "path"
61 "path/filepath"
62 "strings"
63 "time"
64
65 "cmd/internal/browser"
66 "cmd/internal/quoted"
67 "cmd/internal/telemetry/counter"
68 )
69
70 var (
71 unexported bool
72 matchCase bool
73 chdir string
74 showAll bool
75 showCmd bool
76 showSrc bool
77 short bool
78 serveHTTP bool
79 )
80
81
82 func usage() {
83 fmt.Fprintf(os.Stderr, "Usage of [go] doc:\n")
84 fmt.Fprintf(os.Stderr, "\tgo doc\n")
85 fmt.Fprintf(os.Stderr, "\tgo doc <pkg>\n")
86 fmt.Fprintf(os.Stderr, "\tgo doc <sym>[.<methodOrField>]\n")
87 fmt.Fprintf(os.Stderr, "\tgo doc [<pkg>.]<sym>[.<methodOrField>]\n")
88 fmt.Fprintf(os.Stderr, "\tgo doc [<pkg>.][<sym>.]<methodOrField>\n")
89 fmt.Fprintf(os.Stderr, "\tgo doc <pkg> <sym>[.<methodOrField>]\n")
90 fmt.Fprintf(os.Stderr, "For more information run\n")
91 fmt.Fprintf(os.Stderr, "\tgo help doc\n\n")
92 fmt.Fprintf(os.Stderr, "Flags:\n")
93 flag.PrintDefaults()
94 os.Exit(2)
95 }
96
97 func main() {
98 log.SetFlags(0)
99 log.SetPrefix("doc: ")
100 counter.Open()
101 dirsInit()
102 err := do(os.Stdout, flag.CommandLine, os.Args[1:])
103 if err != nil {
104 log.Fatal(err)
105 }
106 }
107
108
109 func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) {
110 flagSet.Usage = usage
111 unexported = false
112 matchCase = false
113 flagSet.StringVar(&chdir, "C", "", "change to `dir` before running command")
114 flagSet.BoolVar(&unexported, "u", false, "show unexported symbols as well as exported")
115 flagSet.BoolVar(&matchCase, "c", false, "symbol matching honors case (paths not affected)")
116 flagSet.BoolVar(&showAll, "all", false, "show all documentation for package")
117 flagSet.BoolVar(&showCmd, "cmd", false, "show symbols with package docs even if package is a command")
118 flagSet.BoolVar(&showSrc, "src", false, "show source code for symbol")
119 flagSet.BoolVar(&short, "short", false, "one-line representation for each symbol")
120 flagSet.BoolVar(&serveHTTP, "http", false, "serve HTML docs over HTTP")
121 flagSet.Parse(args)
122 counter.Inc("doc/invocations")
123 counter.CountFlags("doc/flag:", *flag.CommandLine)
124 if chdir != "" {
125 if err := os.Chdir(chdir); err != nil {
126 return err
127 }
128 }
129 var paths []string
130 var symbol, method string
131
132 dirs.Reset()
133 for i := 0; ; i++ {
134 buildPackage, userPath, sym, more := parseArgs(flagSet.Args())
135 if i > 0 && !more {
136 return failMessage(paths, symbol, method)
137 }
138 if buildPackage == nil {
139 return fmt.Errorf("no such package: %s", userPath)
140 }
141
142
143
144 if buildPackage.ImportPath == "builtin" {
145 unexported = true
146 }
147
148 symbol, method = parseSymbol(sym)
149 pkg := parsePackage(writer, buildPackage, userPath)
150 paths = append(paths, pkg.prettyPath())
151
152 defer func() {
153 pkg.flush()
154 e := recover()
155 if e == nil {
156 return
157 }
158 pkgError, ok := e.(PackageError)
159 if ok {
160 err = pkgError
161 return
162 }
163 panic(e)
164 }()
165
166 if serveHTTP {
167 return doPkgsite(pkg, symbol, method)
168 }
169 switch {
170 case symbol == "":
171 pkg.packageDoc()
172 return
173 case method == "":
174 if pkg.symbolDoc(symbol) {
175 return
176 }
177 case pkg.printMethodDoc(symbol, method):
178 return
179 case pkg.printFieldDoc(symbol, method):
180 return
181 }
182 }
183 }
184
185 func doPkgsite(pkg *Package, symbol, method string) error {
186 ctx := context.Background()
187
188 cmdline := "go run golang.org/x/pkgsite/cmd/pkgsite@latest -gorepo=" + buildCtx.GOROOT
189 words, err := quoted.Split(cmdline)
190 port, err := pickUnusedPort()
191 if err != nil {
192 return fmt.Errorf("failed to find port for documentation server: %v", err)
193 }
194 addr := fmt.Sprintf("localhost:%d", port)
195 words = append(words, fmt.Sprintf("-http=%s", addr))
196 cmd := exec.CommandContext(context.Background(), words[0], words[1:]...)
197 cmd.Stdout = os.Stderr
198 cmd.Stderr = os.Stderr
199
200
201
202 signal.Ignore(signalsToIgnore...)
203
204 if err := cmd.Start(); err != nil {
205 return fmt.Errorf("starting pkgsite: %v", err)
206 }
207
208
209 if !waitAvailable(ctx, addr) {
210 cmd.Cancel()
211 cmd.Wait()
212 return errors.New("could not connect to local documentation server")
213 }
214
215
216 path := path.Join("http://"+addr, pkg.build.ImportPath)
217 object := symbol
218 if symbol != "" && method != "" {
219 object = symbol + "." + method
220 }
221 if object != "" {
222 path = path + "#" + object
223 }
224 if ok := browser.Open(path); !ok {
225 cmd.Cancel()
226 cmd.Wait()
227 return errors.New("failed to open browser")
228 }
229
230
231
232
233 return cmd.Wait()
234 }
235
236
237
238
239
240 func pickUnusedPort() (int, error) {
241 l, err := net.Listen("tcp", "localhost:0")
242 if err != nil {
243 return 0, err
244 }
245 port := l.Addr().(*net.TCPAddr).Port
246 if err := l.Close(); err != nil {
247 return 0, err
248 }
249 return port, nil
250 }
251
252 func waitAvailable(ctx context.Context, addr string) bool {
253 ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
254 defer cancel()
255 for ctx.Err() == nil {
256 req, err := http.NewRequestWithContext(ctx, "HEAD", "http://"+addr, nil)
257 if err != nil {
258 log.Println(err)
259 return false
260 }
261 resp, err := http.DefaultClient.Do(req)
262 if err == nil {
263 resp.Body.Close()
264 return true
265 }
266 }
267 return false
268 }
269
270
271 func failMessage(paths []string, symbol, method string) error {
272 var b bytes.Buffer
273 if len(paths) > 1 {
274 b.WriteString("s")
275 }
276 b.WriteString(" ")
277 for i, path := range paths {
278 if i > 0 {
279 b.WriteString(", ")
280 }
281 b.WriteString(path)
282 }
283 if method == "" {
284 return fmt.Errorf("no symbol %s in package%s", symbol, &b)
285 }
286 return fmt.Errorf("no method or field %s.%s in package%s", symbol, method, &b)
287 }
288
289
290
291
292
293
294
295
296
297
298
299
300 func parseArgs(args []string) (pkg *build.Package, path, symbol string, more bool) {
301 wd, err := os.Getwd()
302 if err != nil {
303 log.Fatal(err)
304 }
305 if len(args) == 0 {
306
307 return importDir(wd), "", "", false
308 }
309 arg := args[0]
310
311
312
313 if isDotSlash(arg) {
314 arg = filepath.Join(wd, arg)
315 }
316 switch len(args) {
317 default:
318 usage()
319 case 1:
320
321 case 2:
322
323 pkg, err := build.Import(args[0], wd, build.ImportComment)
324 if err == nil {
325 return pkg, args[0], args[1], false
326 }
327 for {
328 packagePath, ok := findNextPackage(arg)
329 if !ok {
330 break
331 }
332 if pkg, err := build.ImportDir(packagePath, build.ImportComment); err == nil {
333 return pkg, arg, args[1], true
334 }
335 }
336 return nil, args[0], args[1], false
337 }
338
339
340
341
342
343
344 var importErr error
345 if filepath.IsAbs(arg) {
346 pkg, importErr = build.ImportDir(arg, build.ImportComment)
347 if importErr == nil {
348 return pkg, arg, "", false
349 }
350 } else {
351 pkg, importErr = build.Import(arg, wd, build.ImportComment)
352 if importErr == nil {
353 return pkg, arg, "", false
354 }
355 }
356
357
358
359
360 if !strings.ContainsAny(arg, `/\`) && token.IsExported(arg) {
361 pkg, err := build.ImportDir(".", build.ImportComment)
362 if err == nil {
363 return pkg, "", arg, false
364 }
365 }
366
367
368 slash := strings.LastIndex(arg, "/")
369
370
371
372
373
374 var period int
375
376
377 for start := slash + 1; start < len(arg); start = period + 1 {
378 period = strings.Index(arg[start:], ".")
379 symbol := ""
380 if period < 0 {
381 period = len(arg)
382 } else {
383 period += start
384 symbol = arg[period+1:]
385 }
386
387 pkg, err := build.Import(arg[0:period], wd, build.ImportComment)
388 if err == nil {
389 return pkg, arg[0:period], symbol, false
390 }
391
392
393 pkgName := arg[:period]
394 for {
395 path, ok := findNextPackage(pkgName)
396 if !ok {
397 break
398 }
399 if pkg, err = build.ImportDir(path, build.ImportComment); err == nil {
400 return pkg, arg[0:period], symbol, true
401 }
402 }
403 dirs.Reset()
404 }
405
406 if slash >= 0 {
407
408
409
410
411
412
413 importErrStr := importErr.Error()
414 if strings.Contains(importErrStr, arg[:period]) {
415 log.Fatal(importErrStr)
416 } else {
417 log.Fatalf("no such package %s: %s", arg[:period], importErrStr)
418 }
419 }
420
421 return importDir(wd), "", arg, false
422 }
423
424
425
426
427
428 var dotPaths = []string{
429 `./`,
430 `../`,
431 `.\`,
432 `..\`,
433 }
434
435
436
437 func isDotSlash(arg string) bool {
438 if arg == "." || arg == ".." {
439 return true
440 }
441 for _, dotPath := range dotPaths {
442 if strings.HasPrefix(arg, dotPath) {
443 return true
444 }
445 }
446 return false
447 }
448
449
450 func importDir(dir string) *build.Package {
451 pkg, err := build.ImportDir(dir, build.ImportComment)
452 if err != nil {
453 log.Fatal(err)
454 }
455 return pkg
456 }
457
458
459
460
461 func parseSymbol(str string) (symbol, method string) {
462 if str == "" {
463 return
464 }
465 elem := strings.Split(str, ".")
466 switch len(elem) {
467 case 1:
468 case 2:
469 method = elem[1]
470 default:
471 log.Printf("too many periods in symbol specification")
472 usage()
473 }
474 symbol = elem[0]
475 return
476 }
477
478
479
480
481 func isExported(name string) bool {
482 return unexported || token.IsExported(name)
483 }
484
485
486
487 func findNextPackage(pkg string) (string, bool) {
488 if filepath.IsAbs(pkg) {
489 if dirs.offset == 0 {
490 dirs.offset = -1
491 return pkg, true
492 }
493 return "", false
494 }
495 if pkg == "" || token.IsExported(pkg) {
496 return "", false
497 }
498 pkg = path.Clean(pkg)
499 pkgSuffix := "/" + pkg
500 for {
501 d, ok := dirs.Next()
502 if !ok {
503 return "", false
504 }
505 if d.importPath == pkg || strings.HasSuffix(d.importPath, pkgSuffix) {
506 return d.dir, true
507 }
508 }
509 }
510
511 var buildCtx = build.Default
512
513
514 func splitGopath() []string {
515 return filepath.SplitList(buildCtx.GOPATH)
516 }
517
View as plain text