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