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 gomodcache = filepath.Join(gomodcache, "cache", "download")
231
232
233
234 if strings.HasPrefix(gomodcache, "/") {
235 gomodcache = "file://" + gomodcache
236 } else {
237 gomodcache = "file:///" + filepath.ToSlash(gomodcache)
238 }
239 env = append(env, "GOPROXY="+gomodcache+","+goproxy)
240 }
241
242 const version = "v0.0.0-20250608123103-82c52f1754cd"
243 cmd := exec.Command("go", "run", "golang.org/x/pkgsite/cmd/internal/doc@"+version,
244 "-gorepo", buildCtx.GOROOT,
245 "-http", addr,
246 "-open", path)
247 cmd.Env = env
248 cmd.Stdout = os.Stderr
249 cmd.Stderr = os.Stderr
250
251 if err := cmd.Run(); err != nil {
252 var ee *exec.ExitError
253 if errors.As(err, &ee) {
254
255
256
257
258 os.Exit(ee.ExitCode())
259 }
260 return err
261 }
262
263 return nil
264 }
265
266
267
268
269
270 func pickUnusedPort() (int, error) {
271 l, err := net.Listen("tcp", "localhost:0")
272 if err != nil {
273 return 0, err
274 }
275 port := l.Addr().(*net.TCPAddr).Port
276 if err := l.Close(); err != nil {
277 return 0, err
278 }
279 return port, nil
280 }
281
282
283 func failMessage(paths []string, symbol, method string) error {
284 var b bytes.Buffer
285 if len(paths) > 1 {
286 b.WriteString("s")
287 }
288 b.WriteString(" ")
289 for i, path := range paths {
290 if i > 0 {
291 b.WriteString(", ")
292 }
293 b.WriteString(path)
294 }
295 if method == "" {
296 return fmt.Errorf("no symbol %s in package%s", symbol, &b)
297 }
298 return fmt.Errorf("no method or field %s.%s in package%s", symbol, method, &b)
299 }
300
301
302
303
304
305
306
307
308
309
310
311
312 func parseArgs(flagSet *flag.FlagSet, args []string) (pkg *build.Package, path, symbol string, more bool) {
313 wd, err := os.Getwd()
314 if err != nil {
315 log.Fatal(err)
316 }
317 if len(args) == 0 {
318
319 return importDir(wd), "", "", false
320 }
321 arg := args[0]
322
323
324
325 if isDotSlash(arg) {
326 arg = filepath.Join(wd, arg)
327 }
328 switch len(args) {
329 default:
330 usage(flagSet)
331 case 1:
332
333 case 2:
334
335 pkg, err := build.Import(args[0], wd, build.ImportComment)
336 if err == nil {
337 return pkg, args[0], args[1], false
338 }
339 for {
340 packagePath, ok := findNextPackage(arg)
341 if !ok {
342 break
343 }
344 if pkg, err := build.ImportDir(packagePath, build.ImportComment); err == nil {
345 return pkg, arg, args[1], true
346 }
347 }
348 return nil, args[0], args[1], false
349 }
350
351
352
353
354
355
356 var importErr error
357 if filepath.IsAbs(arg) {
358 pkg, importErr = build.ImportDir(arg, build.ImportComment)
359 if importErr == nil {
360 return pkg, arg, "", false
361 }
362 } else {
363 pkg, importErr = build.Import(arg, wd, build.ImportComment)
364 if importErr == nil {
365 return pkg, arg, "", false
366 }
367 }
368
369
370
371
372 if !strings.ContainsAny(arg, `/\`) && token.IsExported(arg) {
373 pkg, err := build.ImportDir(".", build.ImportComment)
374 if err == nil {
375 return pkg, "", arg, false
376 }
377 }
378
379
380 slash := strings.LastIndex(arg, "/")
381
382
383
384
385
386 var period int
387
388
389 for start := slash + 1; start < len(arg); start = period + 1 {
390 period = strings.Index(arg[start:], ".")
391 symbol := ""
392 if period < 0 {
393 period = len(arg)
394 } else {
395 period += start
396 symbol = arg[period+1:]
397 }
398
399 pkg, err := build.Import(arg[0:period], wd, build.ImportComment)
400 if err == nil {
401 return pkg, arg[0:period], symbol, false
402 }
403
404
405 pkgName := arg[:period]
406 for {
407 path, ok := findNextPackage(pkgName)
408 if !ok {
409 break
410 }
411 if pkg, err = build.ImportDir(path, build.ImportComment); err == nil {
412 return pkg, arg[0:period], symbol, true
413 }
414 }
415 dirs.Reset()
416 }
417
418 if slash >= 0 {
419
420
421
422
423
424
425 importErrStr := importErr.Error()
426 if strings.Contains(importErrStr, arg[:period]) {
427 log.Fatal(importErrStr)
428 } else {
429 log.Fatalf("no such package %s: %s", arg[:period], importErrStr)
430 }
431 }
432
433 return importDir(wd), "", arg, false
434 }
435
436
437
438
439
440 var dotPaths = []string{
441 `./`,
442 `../`,
443 `.\`,
444 `..\`,
445 }
446
447
448
449 func isDotSlash(arg string) bool {
450 if arg == "." || arg == ".." {
451 return true
452 }
453 for _, dotPath := range dotPaths {
454 if strings.HasPrefix(arg, dotPath) {
455 return true
456 }
457 }
458 return false
459 }
460
461
462 func importDir(dir string) *build.Package {
463 pkg, err := build.ImportDir(dir, build.ImportComment)
464 if err != nil {
465 log.Fatal(err)
466 }
467 return pkg
468 }
469
470
471
472
473 func parseSymbol(flagSet *flag.FlagSet, str string) (symbol, method string) {
474 if str == "" {
475 return
476 }
477 elem := strings.Split(str, ".")
478 switch len(elem) {
479 case 1:
480 case 2:
481 method = elem[1]
482 default:
483 log.Printf("too many periods in symbol specification")
484 usage(flagSet)
485 }
486 symbol = elem[0]
487 return
488 }
489
490
491
492
493 func isExported(name string) bool {
494 return unexported || token.IsExported(name)
495 }
496
497
498
499 func findNextPackage(pkg string) (string, bool) {
500 if filepath.IsAbs(pkg) {
501 if dirs.offset == 0 {
502 dirs.offset = -1
503 return pkg, true
504 }
505 return "", false
506 }
507 if pkg == "" || token.IsExported(pkg) {
508 return "", false
509 }
510 pkg = path.Clean(pkg)
511 pkgSuffix := "/" + pkg
512 for {
513 d, ok := dirs.Next()
514 if !ok {
515 return "", false
516 }
517 if d.importPath == pkg || strings.HasSuffix(d.importPath, pkgSuffix) {
518 return d.dir, true
519 }
520 }
521 }
522
523 var buildCtx = build.Default
524
525
526 func splitGopath() []string {
527 return filepath.SplitList(buildCtx.GOPATH)
528 }
529
View as plain text