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