Source file src/cmd/internal/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  // Package doc provides the implementation of the "go doc" subcommand and cmd/doc.
     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   // -u flag
    31  	matchCase  bool   // -c flag
    32  	chdir      string // -C flag
    33  	showAll    bool   // -all flag
    34  	showCmd    bool   // -cmd flag
    35  	showSrc    bool   // -src flag
    36  	short      bool   // -short flag
    37  	serveHTTP  bool   // -http flag
    38  )
    39  
    40  // usage is a replacement usage function for the flags package.
    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  // Main is the entry point, invoked both by go doc and cmd/doc.
    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  // do is the workhorse, broken out of main to make testing easier.
    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  		// Special case: if there are no arguments, try to go to an appropriate page
    90  		// depending on whether we're in a module or workspace. The pkgsite homepage
    91  		// is often not the most useful page.
    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  				// If there's a module, go to the module's doc page.
    96  				return doPkgsite(mod)
    97  			}
    98  			gowork, err := runCmd(nil, "go", "env", "GOWORK")
    99  			if err == nil && gowork != "" {
   100  				// Outside a module, but in a workspace, go to the home page
   101  				// with links to each of the modules' pages.
   102  				return doPkgsite("")
   103  			}
   104  			// Outside a module or workspace, go to the documentation for the standard library.
   105  			return doPkgsite("std")
   106  		}
   107  
   108  		// If args are provided, we need to figure out which page to open on the pkgsite
   109  		// instance. Run the logic below to determine a match for a symbol, method,
   110  		// or field, but don't actually print the documentation to the output.
   111  		writer = io.Discard
   112  	}
   113  	var paths []string
   114  	var symbol, method string
   115  	// Loop until something is printed.
   116  	dirs.Reset()
   117  	for i := 0; ; i++ {
   118  		buildPackage, userPath, sym, more := parseArgs(flagSet, flagSet.Args())
   119  		if i > 0 && !more { // Ignore the "more" bit on the first iteration.
   120  			return failMessage(paths, symbol, method)
   121  		}
   122  		if buildPackage == nil {
   123  			return fmt.Errorf("no such package: %s", userPath)
   124  		}
   125  
   126  		// The builtin package needs special treatment: its symbols are lower
   127  		// case but we want to see them, always.
   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() // The package exists, so we got some output.
   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  		// go/build couldn't determine the import path, probably
   194  		// because this was a relative path into a module. Use
   195  		// go list to get the import path.
   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  	// Turn off the default signal handler for SIGINT (and SIGQUIT on Unix)
   224  	// and instead wait for the child process to handle the signal and
   225  	// exit before exiting ourselves.
   226  	signal.Ignore(signalsToIgnore...)
   227  
   228  	// Prepend the local download cache to GOPROXY to get around deprecation checks.
   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  		// Convert absolute path to file URL. pkgsite will not accept
   236  		// Windows absolute paths because they look like a host:path remote.
   237  		// TODO(golang.org/issue/32456): use url.FromFilePath when implemented.
   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  			// Exit with the same exit status as pkgsite to avoid
   259  			// printing of "exit status" error messages.
   260  			// Any relevant messages have already been printed
   261  			// to stdout or stderr.
   262  			os.Exit(ee.ExitCode())
   263  		}
   264  		return err
   265  	}
   266  
   267  	return nil
   268  }
   269  
   270  // pickUnusedPort finds an unused port by trying to listen on port 0
   271  // and letting the OS pick a port, then closing that connection and
   272  // returning that port number.
   273  // This is inherently racy.
   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  // failMessage creates a nicely formatted error message when there is no result to show.
   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  // parseArgs analyzes the arguments (if any) and returns the package
   306  // it represents, the part of the argument the user used to identify
   307  // the path (or "" if it's the current package) and the symbol
   308  // (possibly with a .method) within that package.
   309  // parseSymbol is used to analyze the symbol itself.
   310  // The boolean final argument reports whether it is possible that
   311  // there may be more directories worth looking at. It will only
   312  // be true if the package path is a partial match for some directory
   313  // and there may be more matches. For example, if the argument
   314  // is rand.Float64, we must scan both crypto/rand and math/rand
   315  // to find the symbol, and the first call will return crypto/rand, true.
   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  		// Easy: current directory.
   323  		return importDir(wd), "", "", false
   324  	}
   325  	arg := args[0]
   326  	// We have an argument. If it is a directory name beginning with . or ..,
   327  	// use the absolute path name. This discriminates "./errors" from "errors"
   328  	// if the current directory contains a non-standard errors package.
   329  	if isDotSlash(arg) {
   330  		arg = filepath.Join(wd, arg)
   331  	}
   332  	switch len(args) {
   333  	default:
   334  		usage(flagSet)
   335  	case 1:
   336  		// Done below.
   337  	case 2:
   338  		// Package must be findable and importable.
   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  	// Usual case: one argument.
   355  	// If it contains slashes, it begins with either a package path
   356  	// or an absolute directory.
   357  	// First, is it a complete package path as it is? If so, we are done.
   358  	// This avoids confusion over package paths that have other
   359  	// package paths as their prefix.
   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  	// Another disambiguator: If the argument starts with an upper
   373  	// case letter, it can only be a symbol in the current directory.
   374  	// Kills the problem caused by case-insensitive file systems
   375  	// matching an upper case name as a package name.
   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  	// If it has a slash, it must be a package path but there is a symbol.
   383  	// It's the last package path we care about.
   384  	slash := strings.LastIndex(arg, "/")
   385  	// There may be periods in the package path before or after the slash
   386  	// and between a symbol and method.
   387  	// Split the string at various periods to see what we find.
   388  	// In general there may be ambiguities but this should almost always
   389  	// work.
   390  	var period int
   391  	// slash+1: if there's no slash, the value is -1 and start is 0; otherwise
   392  	// start is the byte after the slash.
   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  		// Have we identified a package already?
   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  		// See if we have the basename or tail of a package, as in json for encoding/json
   408  		// or ivy/value for robpike.io/ivy/value.
   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() // Next iteration of for loop must scan all the directories again.
   420  	}
   421  	// If it has a slash, we've failed.
   422  	if slash >= 0 {
   423  		// build.Import should always include the path in its error message,
   424  		// and we should avoid repeating it. Unfortunately, build.Import doesn't
   425  		// return a structured error. That can't easily be fixed, since it
   426  		// invokes 'go list' and returns the error text from the loaded package.
   427  		// TODO(golang.org/issue/34750): load using golang.org/x/tools/go/packages
   428  		// instead of go/build.
   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  	// Guess it's a symbol in the current directory.
   437  	return importDir(wd), "", arg, false
   438  }
   439  
   440  // dotPaths lists all the dotted paths legal on Unix-like and
   441  // Windows-like file systems. We check them all, as the chance
   442  // of error is minute and even on Windows people will use ./
   443  // sometimes.
   444  var dotPaths = []string{
   445  	`./`,
   446  	`../`,
   447  	`.\`,
   448  	`..\`,
   449  }
   450  
   451  // isDotSlash reports whether the path begins with a reference
   452  // to the local . or .. directory.
   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  // importDir is just an error-catching wrapper for build.ImportDir.
   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  // parseSymbol breaks str apart into a symbol and method.
   475  // Both may be missing or the method may be missing.
   476  // If present, each must be a valid Go identifier.
   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  // isExported reports whether the name is an exported identifier.
   495  // If the unexported flag (-u) is true, isExported returns true because
   496  // it means that we treat the name as if it is exported.
   497  func isExported(name string) bool {
   498  	return unexported || token.IsExported(name)
   499  }
   500  
   501  // findNextPackage returns the next full file name path that matches the
   502  // (perhaps partial) package path pkg. The boolean reports if any match was found.
   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) { // Upper case symbol cannot be a package name.
   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  // splitGopath splits $GOPATH into a list of roots.
   530  func splitGopath() []string {
   531  	return filepath.SplitList(buildCtx.GOPATH)
   532  }
   533  

View as plain text