Source file src/cmd/doc/main.go

     1  // Copyright 2015 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Doc (usually run as go doc) accepts zero, one or two arguments.
     6  //
     7  // Zero arguments:
     8  //
     9  //	go doc
    10  //
    11  // Show the documentation for the package in the current directory.
    12  //
    13  // One argument:
    14  //
    15  //	go doc <pkg>
    16  //	go doc <sym>[.<methodOrField>]
    17  //	go doc [<pkg>.]<sym>[.<methodOrField>]
    18  //	go doc [<pkg>.][<sym>.]<methodOrField>
    19  //
    20  // The first item in this list that succeeds is the one whose documentation
    21  // is printed. If there is a symbol but no package, the package in the current
    22  // directory is chosen. However, if the argument begins with a capital
    23  // letter it is always assumed to be a symbol in the current directory.
    24  //
    25  // Two arguments:
    26  //
    27  //	go doc <pkg> <sym>[.<methodOrField>]
    28  //
    29  // Show the documentation for the package, symbol, and method or field. The
    30  // first argument must be a full package path. This is similar to the
    31  // command-line usage for the godoc command.
    32  //
    33  // For commands, unless the -cmd flag is present "go doc command"
    34  // shows only the package-level docs for the package.
    35  //
    36  // The -src flag causes doc to print the full source code for the symbol, such
    37  // as the body of a struct, function or method.
    38  //
    39  // The -all flag causes doc to print all documentation for the package and
    40  // all its visible symbols. The argument must identify a package.
    41  //
    42  // For complete documentation, run "go help doc".
    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   // -u flag
    72  	matchCase  bool   // -c flag
    73  	chdir      string // -C flag
    74  	showAll    bool   // -all flag
    75  	showCmd    bool   // -cmd flag
    76  	showSrc    bool   // -src flag
    77  	short      bool   // -short flag
    78  	serveHTTP  bool   // -http flag
    79  )
    80  
    81  // usage is a replacement usage function for the flags package.
    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  // do is the workhorse, broken out of main to make testing easier.
   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  	// Loop until something is printed.
   132  	dirs.Reset()
   133  	for i := 0; ; i++ {
   134  		buildPackage, userPath, sym, more := parseArgs(flagSet.Args())
   135  		if i > 0 && !more { // Ignore the "more" bit on the first iteration.
   136  			return failMessage(paths, symbol, method)
   137  		}
   138  		if buildPackage == nil {
   139  			return fmt.Errorf("no such package: %s", userPath)
   140  		}
   141  
   142  		// The builtin package needs special treatment: its symbols are lower
   143  		// case but we want to see them, always.
   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() // The package exists, so we got some output.
   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  	// Turn off the default signal handler for SIGINT (and SIGQUIT on Unix)
   200  	// and instead wait for the child process to handle the signal and
   201  	// exit before exiting ourselves.
   202  	signal.Ignore(signalsToIgnore...)
   203  
   204  	if err := cmd.Start(); err != nil {
   205  		return fmt.Errorf("starting pkgsite: %v", err)
   206  	}
   207  
   208  	// Wait for pkgsite to became available.
   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  	// Open web browser.
   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  	// Wait for child to terminate. We expect the child process to receive signals from
   231  	// this terminal and terminate in a timely manner, so this process will terminate
   232  	// soon after.
   233  	return cmd.Wait()
   234  }
   235  
   236  // pickUnusedPort finds an unused port by trying to listen on port 0
   237  // and letting the OS pick a port, then closing that connection and
   238  // returning that port number.
   239  // This is inherently racy.
   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  // failMessage creates a nicely formatted error message when there is no result to show.
   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  // parseArgs analyzes the arguments (if any) and returns the package
   290  // it represents, the part of the argument the user used to identify
   291  // the path (or "" if it's the current package) and the symbol
   292  // (possibly with a .method) within that package.
   293  // parseSymbol is used to analyze the symbol itself.
   294  // The boolean final argument reports whether it is possible that
   295  // there may be more directories worth looking at. It will only
   296  // be true if the package path is a partial match for some directory
   297  // and there may be more matches. For example, if the argument
   298  // is rand.Float64, we must scan both crypto/rand and math/rand
   299  // to find the symbol, and the first call will return crypto/rand, true.
   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  		// Easy: current directory.
   307  		return importDir(wd), "", "", false
   308  	}
   309  	arg := args[0]
   310  	// We have an argument. If it is a directory name beginning with . or ..,
   311  	// use the absolute path name. This discriminates "./errors" from "errors"
   312  	// if the current directory contains a non-standard errors package.
   313  	if isDotSlash(arg) {
   314  		arg = filepath.Join(wd, arg)
   315  	}
   316  	switch len(args) {
   317  	default:
   318  		usage()
   319  	case 1:
   320  		// Done below.
   321  	case 2:
   322  		// Package must be findable and importable.
   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  	// Usual case: one argument.
   339  	// If it contains slashes, it begins with either a package path
   340  	// or an absolute directory.
   341  	// First, is it a complete package path as it is? If so, we are done.
   342  	// This avoids confusion over package paths that have other
   343  	// package paths as their prefix.
   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  	// Another disambiguator: If the argument starts with an upper
   357  	// case letter, it can only be a symbol in the current directory.
   358  	// Kills the problem caused by case-insensitive file systems
   359  	// matching an upper case name as a package name.
   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  	// If it has a slash, it must be a package path but there is a symbol.
   367  	// It's the last package path we care about.
   368  	slash := strings.LastIndex(arg, "/")
   369  	// There may be periods in the package path before or after the slash
   370  	// and between a symbol and method.
   371  	// Split the string at various periods to see what we find.
   372  	// In general there may be ambiguities but this should almost always
   373  	// work.
   374  	var period int
   375  	// slash+1: if there's no slash, the value is -1 and start is 0; otherwise
   376  	// start is the byte after the slash.
   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  		// Have we identified a package already?
   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  		// See if we have the basename or tail of a package, as in json for encoding/json
   392  		// or ivy/value for robpike.io/ivy/value.
   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() // Next iteration of for loop must scan all the directories again.
   404  	}
   405  	// If it has a slash, we've failed.
   406  	if slash >= 0 {
   407  		// build.Import should always include the path in its error message,
   408  		// and we should avoid repeating it. Unfortunately, build.Import doesn't
   409  		// return a structured error. That can't easily be fixed, since it
   410  		// invokes 'go list' and returns the error text from the loaded package.
   411  		// TODO(golang.org/issue/34750): load using golang.org/x/tools/go/packages
   412  		// instead of go/build.
   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  	// Guess it's a symbol in the current directory.
   421  	return importDir(wd), "", arg, false
   422  }
   423  
   424  // dotPaths lists all the dotted paths legal on Unix-like and
   425  // Windows-like file systems. We check them all, as the chance
   426  // of error is minute and even on Windows people will use ./
   427  // sometimes.
   428  var dotPaths = []string{
   429  	`./`,
   430  	`../`,
   431  	`.\`,
   432  	`..\`,
   433  }
   434  
   435  // isDotSlash reports whether the path begins with a reference
   436  // to the local . or .. directory.
   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  // importDir is just an error-catching wrapper for build.ImportDir.
   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  // parseSymbol breaks str apart into a symbol and method.
   459  // Both may be missing or the method may be missing.
   460  // If present, each must be a valid Go identifier.
   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  // isExported reports whether the name is an exported identifier.
   479  // If the unexported flag (-u) is true, isExported returns true because
   480  // it means that we treat the name as if it is exported.
   481  func isExported(name string) bool {
   482  	return unexported || token.IsExported(name)
   483  }
   484  
   485  // findNextPackage returns the next full file name path that matches the
   486  // (perhaps partial) package path pkg. The boolean reports if any match was found.
   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) { // Upper case symbol cannot be a package name.
   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  // splitGopath splits $GOPATH into a list of roots.
   514  func splitGopath() []string {
   515  	return filepath.SplitList(buildCtx.GOPATH)
   516  }
   517  

View as plain text