Black Lives Matter. Support the Equal Justice Initiative.

Source file src/cmd/doc/main.go

Documentation: cmd/doc

     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  //	go doc
     9  // Show the documentation for the package in the current directory.
    10  //
    11  // One argument:
    12  //	go doc <pkg>
    13  //	go doc <sym>[.<methodOrField>]
    14  //	go doc [<pkg>.]<sym>[.<methodOrField>]
    15  //	go doc [<pkg>.][<sym>.]<methodOrField>
    16  // The first item in this list that succeeds is the one whose documentation
    17  // is printed. If there is a symbol but no package, the package in the current
    18  // directory is chosen. However, if the argument begins with a capital
    19  // letter it is always assumed to be a symbol in the current directory.
    20  //
    21  // Two arguments:
    22  //	go doc <pkg> <sym>[.<methodOrField>]
    23  //
    24  // Show the documentation for the package, symbol, and method or field. The
    25  // first argument must be a full package path. This is similar to the
    26  // command-line usage for the godoc command.
    27  //
    28  // For commands, unless the -cmd flag is present "go doc command"
    29  // shows only the package-level docs for the package.
    30  //
    31  // The -src flag causes doc to print the full source code for the symbol, such
    32  // as the body of a struct, function or method.
    33  //
    34  // The -all flag causes doc to print all documentation for the package and
    35  // all its visible symbols. The argument must identify a package.
    36  //
    37  // For complete documentation, run "go help doc".
    38  package main
    39  
    40  import (
    41  	"bytes"
    42  	"flag"
    43  	"fmt"
    44  	"go/build"
    45  	"go/token"
    46  	"io"
    47  	"log"
    48  	"os"
    49  	"path"
    50  	"path/filepath"
    51  	"strings"
    52  )
    53  
    54  var (
    55  	unexported bool // -u flag
    56  	matchCase  bool // -c flag
    57  	showAll    bool // -all flag
    58  	showCmd    bool // -cmd flag
    59  	showSrc    bool // -src flag
    60  	short      bool // -short flag
    61  )
    62  
    63  // usage is a replacement usage function for the flags package.
    64  func usage() {
    65  	fmt.Fprintf(os.Stderr, "Usage of [go] doc:\n")
    66  	fmt.Fprintf(os.Stderr, "\tgo doc\n")
    67  	fmt.Fprintf(os.Stderr, "\tgo doc <pkg>\n")
    68  	fmt.Fprintf(os.Stderr, "\tgo doc <sym>[.<methodOrField>]\n")
    69  	fmt.Fprintf(os.Stderr, "\tgo doc [<pkg>.]<sym>[.<methodOrField>]\n")
    70  	fmt.Fprintf(os.Stderr, "\tgo doc [<pkg>.][<sym>.]<methodOrField>\n")
    71  	fmt.Fprintf(os.Stderr, "\tgo doc <pkg> <sym>[.<methodOrField>]\n")
    72  	fmt.Fprintf(os.Stderr, "For more information run\n")
    73  	fmt.Fprintf(os.Stderr, "\tgo help doc\n\n")
    74  	fmt.Fprintf(os.Stderr, "Flags:\n")
    75  	flag.PrintDefaults()
    76  	os.Exit(2)
    77  }
    78  
    79  func main() {
    80  	log.SetFlags(0)
    81  	log.SetPrefix("doc: ")
    82  	dirsInit()
    83  	err := do(os.Stdout, flag.CommandLine, os.Args[1:])
    84  	if err != nil {
    85  		log.Fatal(err)
    86  	}
    87  }
    88  
    89  // do is the workhorse, broken out of main to make testing easier.
    90  func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) {
    91  	flagSet.Usage = usage
    92  	unexported = false
    93  	matchCase = false
    94  	flagSet.BoolVar(&unexported, "u", false, "show unexported symbols as well as exported")
    95  	flagSet.BoolVar(&matchCase, "c", false, "symbol matching honors case (paths not affected)")
    96  	flagSet.BoolVar(&showAll, "all", false, "show all documentation for package")
    97  	flagSet.BoolVar(&showCmd, "cmd", false, "show symbols with package docs even if package is a command")
    98  	flagSet.BoolVar(&showSrc, "src", false, "show source code for symbol")
    99  	flagSet.BoolVar(&short, "short", false, "one-line representation for each symbol")
   100  	flagSet.Parse(args)
   101  	var paths []string
   102  	var symbol, method string
   103  	// Loop until something is printed.
   104  	dirs.Reset()
   105  	for i := 0; ; i++ {
   106  		buildPackage, userPath, sym, more := parseArgs(flagSet.Args())
   107  		if i > 0 && !more { // Ignore the "more" bit on the first iteration.
   108  			return failMessage(paths, symbol, method)
   109  		}
   110  		if buildPackage == nil {
   111  			return fmt.Errorf("no such package: %s", userPath)
   112  		}
   113  		symbol, method = parseSymbol(sym)
   114  		pkg := parsePackage(writer, buildPackage, userPath)
   115  		paths = append(paths, pkg.prettyPath())
   116  
   117  		defer func() {
   118  			pkg.flush()
   119  			e := recover()
   120  			if e == nil {
   121  				return
   122  			}
   123  			pkgError, ok := e.(PackageError)
   124  			if ok {
   125  				err = pkgError
   126  				return
   127  			}
   128  			panic(e)
   129  		}()
   130  
   131  		// The builtin package needs special treatment: its symbols are lower
   132  		// case but we want to see them, always.
   133  		if pkg.build.ImportPath == "builtin" {
   134  			unexported = true
   135  		}
   136  
   137  		// We have a package.
   138  		if showAll && symbol == "" {
   139  			pkg.allDoc()
   140  			return
   141  		}
   142  
   143  		switch {
   144  		case symbol == "":
   145  			pkg.packageDoc() // The package exists, so we got some output.
   146  			return
   147  		case method == "":
   148  			if pkg.symbolDoc(symbol) {
   149  				return
   150  			}
   151  		default:
   152  			if pkg.methodDoc(symbol, method) {
   153  				return
   154  			}
   155  			if pkg.fieldDoc(symbol, method) {
   156  				return
   157  			}
   158  		}
   159  	}
   160  }
   161  
   162  // failMessage creates a nicely formatted error message when there is no result to show.
   163  func failMessage(paths []string, symbol, method string) error {
   164  	var b bytes.Buffer
   165  	if len(paths) > 1 {
   166  		b.WriteString("s")
   167  	}
   168  	b.WriteString(" ")
   169  	for i, path := range paths {
   170  		if i > 0 {
   171  			b.WriteString(", ")
   172  		}
   173  		b.WriteString(path)
   174  	}
   175  	if method == "" {
   176  		return fmt.Errorf("no symbol %s in package%s", symbol, &b)
   177  	}
   178  	return fmt.Errorf("no method or field %s.%s in package%s", symbol, method, &b)
   179  }
   180  
   181  // parseArgs analyzes the arguments (if any) and returns the package
   182  // it represents, the part of the argument the user used to identify
   183  // the path (or "" if it's the current package) and the symbol
   184  // (possibly with a .method) within that package.
   185  // parseSymbol is used to analyze the symbol itself.
   186  // The boolean final argument reports whether it is possible that
   187  // there may be more directories worth looking at. It will only
   188  // be true if the package path is a partial match for some directory
   189  // and there may be more matches. For example, if the argument
   190  // is rand.Float64, we must scan both crypto/rand and math/rand
   191  // to find the symbol, and the first call will return crypto/rand, true.
   192  func parseArgs(args []string) (pkg *build.Package, path, symbol string, more bool) {
   193  	wd, err := os.Getwd()
   194  	if err != nil {
   195  		log.Fatal(err)
   196  	}
   197  	if len(args) == 0 {
   198  		// Easy: current directory.
   199  		return importDir(wd), "", "", false
   200  	}
   201  	arg := args[0]
   202  	// We have an argument. If it is a directory name beginning with . or ..,
   203  	// use the absolute path name. This discriminates "./errors" from "errors"
   204  	// if the current directory contains a non-standard errors package.
   205  	if isDotSlash(arg) {
   206  		arg = filepath.Join(wd, arg)
   207  	}
   208  	switch len(args) {
   209  	default:
   210  		usage()
   211  	case 1:
   212  		// Done below.
   213  	case 2:
   214  		// Package must be findable and importable.
   215  		pkg, err := build.Import(args[0], wd, build.ImportComment)
   216  		if err == nil {
   217  			return pkg, args[0], args[1], false
   218  		}
   219  		for {
   220  			packagePath, ok := findNextPackage(arg)
   221  			if !ok {
   222  				break
   223  			}
   224  			if pkg, err := build.ImportDir(packagePath, build.ImportComment); err == nil {
   225  				return pkg, arg, args[1], true
   226  			}
   227  		}
   228  		return nil, args[0], args[1], false
   229  	}
   230  	// Usual case: one argument.
   231  	// If it contains slashes, it begins with either a package path
   232  	// or an absolute directory.
   233  	// First, is it a complete package path as it is? If so, we are done.
   234  	// This avoids confusion over package paths that have other
   235  	// package paths as their prefix.
   236  	var importErr error
   237  	if filepath.IsAbs(arg) {
   238  		pkg, importErr = build.ImportDir(arg, build.ImportComment)
   239  		if importErr == nil {
   240  			return pkg, arg, "", false
   241  		}
   242  	} else {
   243  		pkg, importErr = build.Import(arg, wd, build.ImportComment)
   244  		if importErr == nil {
   245  			return pkg, arg, "", false
   246  		}
   247  	}
   248  	// Another disambiguator: If the argument starts with an upper
   249  	// case letter, it can only be a symbol in the current directory.
   250  	// Kills the problem caused by case-insensitive file systems
   251  	// matching an upper case name as a package name.
   252  	if !strings.ContainsAny(arg, `/\`) && token.IsExported(arg) {
   253  		pkg, err := build.ImportDir(".", build.ImportComment)
   254  		if err == nil {
   255  			return pkg, "", arg, false
   256  		}
   257  	}
   258  	// If it has a slash, it must be a package path but there is a symbol.
   259  	// It's the last package path we care about.
   260  	slash := strings.LastIndex(arg, "/")
   261  	// There may be periods in the package path before or after the slash
   262  	// and between a symbol and method.
   263  	// Split the string at various periods to see what we find.
   264  	// In general there may be ambiguities but this should almost always
   265  	// work.
   266  	var period int
   267  	// slash+1: if there's no slash, the value is -1 and start is 0; otherwise
   268  	// start is the byte after the slash.
   269  	for start := slash + 1; start < len(arg); start = period + 1 {
   270  		period = strings.Index(arg[start:], ".")
   271  		symbol := ""
   272  		if period < 0 {
   273  			period = len(arg)
   274  		} else {
   275  			period += start
   276  			symbol = arg[period+1:]
   277  		}
   278  		// Have we identified a package already?
   279  		pkg, err := build.Import(arg[0:period], wd, build.ImportComment)
   280  		if err == nil {
   281  			return pkg, arg[0:period], symbol, false
   282  		}
   283  		// See if we have the basename or tail of a package, as in json for encoding/json
   284  		// or ivy/value for robpike.io/ivy/value.
   285  		pkgName := arg[:period]
   286  		for {
   287  			path, ok := findNextPackage(pkgName)
   288  			if !ok {
   289  				break
   290  			}
   291  			if pkg, err = build.ImportDir(path, build.ImportComment); err == nil {
   292  				return pkg, arg[0:period], symbol, true
   293  			}
   294  		}
   295  		dirs.Reset() // Next iteration of for loop must scan all the directories again.
   296  	}
   297  	// If it has a slash, we've failed.
   298  	if slash >= 0 {
   299  		// build.Import should always include the path in its error message,
   300  		// and we should avoid repeating it. Unfortunately, build.Import doesn't
   301  		// return a structured error. That can't easily be fixed, since it
   302  		// invokes 'go list' and returns the error text from the loaded package.
   303  		// TODO(golang.org/issue/34750): load using golang.org/x/tools/go/packages
   304  		// instead of go/build.
   305  		importErrStr := importErr.Error()
   306  		if strings.Contains(importErrStr, arg[:period]) {
   307  			log.Fatal(importErrStr)
   308  		} else {
   309  			log.Fatalf("no such package %s: %s", arg[:period], importErrStr)
   310  		}
   311  	}
   312  	// Guess it's a symbol in the current directory.
   313  	return importDir(wd), "", arg, false
   314  }
   315  
   316  // dotPaths lists all the dotted paths legal on Unix-like and
   317  // Windows-like file systems. We check them all, as the chance
   318  // of error is minute and even on Windows people will use ./
   319  // sometimes.
   320  var dotPaths = []string{
   321  	`./`,
   322  	`../`,
   323  	`.\`,
   324  	`..\`,
   325  }
   326  
   327  // isDotSlash reports whether the path begins with a reference
   328  // to the local . or .. directory.
   329  func isDotSlash(arg string) bool {
   330  	if arg == "." || arg == ".." {
   331  		return true
   332  	}
   333  	for _, dotPath := range dotPaths {
   334  		if strings.HasPrefix(arg, dotPath) {
   335  			return true
   336  		}
   337  	}
   338  	return false
   339  }
   340  
   341  // importDir is just an error-catching wrapper for build.ImportDir.
   342  func importDir(dir string) *build.Package {
   343  	pkg, err := build.ImportDir(dir, build.ImportComment)
   344  	if err != nil {
   345  		log.Fatal(err)
   346  	}
   347  	return pkg
   348  }
   349  
   350  // parseSymbol breaks str apart into a symbol and method.
   351  // Both may be missing or the method may be missing.
   352  // If present, each must be a valid Go identifier.
   353  func parseSymbol(str string) (symbol, method string) {
   354  	if str == "" {
   355  		return
   356  	}
   357  	elem := strings.Split(str, ".")
   358  	switch len(elem) {
   359  	case 1:
   360  	case 2:
   361  		method = elem[1]
   362  	default:
   363  		log.Printf("too many periods in symbol specification")
   364  		usage()
   365  	}
   366  	symbol = elem[0]
   367  	return
   368  }
   369  
   370  // isExported reports whether the name is an exported identifier.
   371  // If the unexported flag (-u) is true, isExported returns true because
   372  // it means that we treat the name as if it is exported.
   373  func isExported(name string) bool {
   374  	return unexported || token.IsExported(name)
   375  }
   376  
   377  // findNextPackage returns the next full file name path that matches the
   378  // (perhaps partial) package path pkg. The boolean reports if any match was found.
   379  func findNextPackage(pkg string) (string, bool) {
   380  	if filepath.IsAbs(pkg) {
   381  		if dirs.offset == 0 {
   382  			dirs.offset = -1
   383  			return pkg, true
   384  		}
   385  		return "", false
   386  	}
   387  	if pkg == "" || token.IsExported(pkg) { // Upper case symbol cannot be a package name.
   388  		return "", false
   389  	}
   390  	pkg = path.Clean(pkg)
   391  	pkgSuffix := "/" + pkg
   392  	for {
   393  		d, ok := dirs.Next()
   394  		if !ok {
   395  			return "", false
   396  		}
   397  		if d.importPath == pkg || strings.HasSuffix(d.importPath, pkgSuffix) {
   398  			return d.dir, true
   399  		}
   400  	}
   401  }
   402  
   403  var buildCtx = build.Default
   404  
   405  // splitGopath splits $GOPATH into a list of roots.
   406  func splitGopath() []string {
   407  	return filepath.SplitList(buildCtx.GOPATH)
   408  }
   409  

View as plain text