diff options
Diffstat (limited to '')
-rw-r--r-- | src/cmd/doc/main.go | 408 |
1 files changed, 408 insertions, 0 deletions
diff --git a/src/cmd/doc/main.go b/src/cmd/doc/main.go new file mode 100644 index 0000000..0499c40 --- /dev/null +++ b/src/cmd/doc/main.go @@ -0,0 +1,408 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Doc (usually run as go doc) accepts zero, one or two arguments. +// +// Zero arguments: +// go doc +// Show the documentation for the package in the current directory. +// +// One argument: +// go doc <pkg> +// go doc <sym>[.<methodOrField>] +// go doc [<pkg>.]<sym>[.<methodOrField>] +// go doc [<pkg>.][<sym>.]<methodOrField> +// The first item in this list that succeeds is the one whose documentation +// is printed. If there is a symbol but no package, the package in the current +// directory is chosen. However, if the argument begins with a capital +// letter it is always assumed to be a symbol in the current directory. +// +// Two arguments: +// go doc <pkg> <sym>[.<methodOrField>] +// +// Show the documentation for the package, symbol, and method or field. The +// first argument must be a full package path. This is similar to the +// command-line usage for the godoc command. +// +// For commands, unless the -cmd flag is present "go doc command" +// shows only the package-level docs for the package. +// +// The -src flag causes doc to print the full source code for the symbol, such +// as the body of a struct, function or method. +// +// The -all flag causes doc to print all documentation for the package and +// all its visible symbols. The argument must identify a package. +// +// For complete documentation, run "go help doc". +package main + +import ( + "bytes" + "flag" + "fmt" + "go/build" + "go/token" + "io" + "log" + "os" + "path" + "path/filepath" + "strings" +) + +var ( + unexported bool // -u flag + matchCase bool // -c flag + showAll bool // -all flag + showCmd bool // -cmd flag + showSrc bool // -src flag + short bool // -short flag +) + +// usage is a replacement usage function for the flags package. +func usage() { + fmt.Fprintf(os.Stderr, "Usage of [go] doc:\n") + fmt.Fprintf(os.Stderr, "\tgo doc\n") + fmt.Fprintf(os.Stderr, "\tgo doc <pkg>\n") + fmt.Fprintf(os.Stderr, "\tgo doc <sym>[.<methodOrField>]\n") + fmt.Fprintf(os.Stderr, "\tgo doc [<pkg>.]<sym>[.<methodOrField>]\n") + fmt.Fprintf(os.Stderr, "\tgo doc [<pkg>.][<sym>.]<methodOrField>\n") + fmt.Fprintf(os.Stderr, "\tgo doc <pkg> <sym>[.<methodOrField>]\n") + fmt.Fprintf(os.Stderr, "For more information run\n") + fmt.Fprintf(os.Stderr, "\tgo help doc\n\n") + fmt.Fprintf(os.Stderr, "Flags:\n") + flag.PrintDefaults() + os.Exit(2) +} + +func main() { + log.SetFlags(0) + log.SetPrefix("doc: ") + dirsInit() + err := do(os.Stdout, flag.CommandLine, os.Args[1:]) + if err != nil { + log.Fatal(err) + } +} + +// do is the workhorse, broken out of main to make testing easier. +func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) { + flagSet.Usage = usage + unexported = false + matchCase = false + flagSet.BoolVar(&unexported, "u", false, "show unexported symbols as well as exported") + flagSet.BoolVar(&matchCase, "c", false, "symbol matching honors case (paths not affected)") + flagSet.BoolVar(&showAll, "all", false, "show all documentation for package") + flagSet.BoolVar(&showCmd, "cmd", false, "show symbols with package docs even if package is a command") + flagSet.BoolVar(&showSrc, "src", false, "show source code for symbol") + flagSet.BoolVar(&short, "short", false, "one-line representation for each symbol") + flagSet.Parse(args) + var paths []string + var symbol, method string + // Loop until something is printed. + dirs.Reset() + for i := 0; ; i++ { + buildPackage, userPath, sym, more := parseArgs(flagSet.Args()) + if i > 0 && !more { // Ignore the "more" bit on the first iteration. + return failMessage(paths, symbol, method) + } + if buildPackage == nil { + return fmt.Errorf("no such package: %s", userPath) + } + symbol, method = parseSymbol(sym) + pkg := parsePackage(writer, buildPackage, userPath) + paths = append(paths, pkg.prettyPath()) + + defer func() { + pkg.flush() + e := recover() + if e == nil { + return + } + pkgError, ok := e.(PackageError) + if ok { + err = pkgError + return + } + panic(e) + }() + + // The builtin package needs special treatment: its symbols are lower + // case but we want to see them, always. + if pkg.build.ImportPath == "builtin" { + unexported = true + } + + // We have a package. + if showAll && symbol == "" { + pkg.allDoc() + return + } + + switch { + case symbol == "": + pkg.packageDoc() // The package exists, so we got some output. + return + case method == "": + if pkg.symbolDoc(symbol) { + return + } + default: + if pkg.methodDoc(symbol, method) { + return + } + if pkg.fieldDoc(symbol, method) { + return + } + } + } +} + +// failMessage creates a nicely formatted error message when there is no result to show. +func failMessage(paths []string, symbol, method string) error { + var b bytes.Buffer + if len(paths) > 1 { + b.WriteString("s") + } + b.WriteString(" ") + for i, path := range paths { + if i > 0 { + b.WriteString(", ") + } + b.WriteString(path) + } + if method == "" { + return fmt.Errorf("no symbol %s in package%s", symbol, &b) + } + return fmt.Errorf("no method or field %s.%s in package%s", symbol, method, &b) +} + +// parseArgs analyzes the arguments (if any) and returns the package +// it represents, the part of the argument the user used to identify +// the path (or "" if it's the current package) and the symbol +// (possibly with a .method) within that package. +// parseSymbol is used to analyze the symbol itself. +// The boolean final argument reports whether it is possible that +// there may be more directories worth looking at. It will only +// be true if the package path is a partial match for some directory +// and there may be more matches. For example, if the argument +// is rand.Float64, we must scan both crypto/rand and math/rand +// to find the symbol, and the first call will return crypto/rand, true. +func parseArgs(args []string) (pkg *build.Package, path, symbol string, more bool) { + wd, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + if len(args) == 0 { + // Easy: current directory. + return importDir(wd), "", "", false + } + arg := args[0] + // We have an argument. If it is a directory name beginning with . or .., + // use the absolute path name. This discriminates "./errors" from "errors" + // if the current directory contains a non-standard errors package. + if isDotSlash(arg) { + arg = filepath.Join(wd, arg) + } + switch len(args) { + default: + usage() + case 1: + // Done below. + case 2: + // Package must be findable and importable. + pkg, err := build.Import(args[0], wd, build.ImportComment) + if err == nil { + return pkg, args[0], args[1], false + } + for { + packagePath, ok := findNextPackage(arg) + if !ok { + break + } + if pkg, err := build.ImportDir(packagePath, build.ImportComment); err == nil { + return pkg, arg, args[1], true + } + } + return nil, args[0], args[1], false + } + // Usual case: one argument. + // If it contains slashes, it begins with either a package path + // or an absolute directory. + // First, is it a complete package path as it is? If so, we are done. + // This avoids confusion over package paths that have other + // package paths as their prefix. + var importErr error + if filepath.IsAbs(arg) { + pkg, importErr = build.ImportDir(arg, build.ImportComment) + if importErr == nil { + return pkg, arg, "", false + } + } else { + pkg, importErr = build.Import(arg, wd, build.ImportComment) + if importErr == nil { + return pkg, arg, "", false + } + } + // Another disambiguator: If the argument starts with an upper + // case letter, it can only be a symbol in the current directory. + // Kills the problem caused by case-insensitive file systems + // matching an upper case name as a package name. + if !strings.ContainsAny(arg, `/\`) && token.IsExported(arg) { + pkg, err := build.ImportDir(".", build.ImportComment) + if err == nil { + return pkg, "", arg, false + } + } + // If it has a slash, it must be a package path but there is a symbol. + // It's the last package path we care about. + slash := strings.LastIndex(arg, "/") + // There may be periods in the package path before or after the slash + // and between a symbol and method. + // Split the string at various periods to see what we find. + // In general there may be ambiguities but this should almost always + // work. + var period int + // slash+1: if there's no slash, the value is -1 and start is 0; otherwise + // start is the byte after the slash. + for start := slash + 1; start < len(arg); start = period + 1 { + period = strings.Index(arg[start:], ".") + symbol := "" + if period < 0 { + period = len(arg) + } else { + period += start + symbol = arg[period+1:] + } + // Have we identified a package already? + pkg, err := build.Import(arg[0:period], wd, build.ImportComment) + if err == nil { + return pkg, arg[0:period], symbol, false + } + // See if we have the basename or tail of a package, as in json for encoding/json + // or ivy/value for robpike.io/ivy/value. + pkgName := arg[:period] + for { + path, ok := findNextPackage(pkgName) + if !ok { + break + } + if pkg, err = build.ImportDir(path, build.ImportComment); err == nil { + return pkg, arg[0:period], symbol, true + } + } + dirs.Reset() // Next iteration of for loop must scan all the directories again. + } + // If it has a slash, we've failed. + if slash >= 0 { + // build.Import should always include the path in its error message, + // and we should avoid repeating it. Unfortunately, build.Import doesn't + // return a structured error. That can't easily be fixed, since it + // invokes 'go list' and returns the error text from the loaded package. + // TODO(golang.org/issue/34750): load using golang.org/x/tools/go/packages + // instead of go/build. + importErrStr := importErr.Error() + if strings.Contains(importErrStr, arg[:period]) { + log.Fatal(importErrStr) + } else { + log.Fatalf("no such package %s: %s", arg[:period], importErrStr) + } + } + // Guess it's a symbol in the current directory. + return importDir(wd), "", arg, false +} + +// dotPaths lists all the dotted paths legal on Unix-like and +// Windows-like file systems. We check them all, as the chance +// of error is minute and even on Windows people will use ./ +// sometimes. +var dotPaths = []string{ + `./`, + `../`, + `.\`, + `..\`, +} + +// isDotSlash reports whether the path begins with a reference +// to the local . or .. directory. +func isDotSlash(arg string) bool { + if arg == "." || arg == ".." { + return true + } + for _, dotPath := range dotPaths { + if strings.HasPrefix(arg, dotPath) { + return true + } + } + return false +} + +// importDir is just an error-catching wrapper for build.ImportDir. +func importDir(dir string) *build.Package { + pkg, err := build.ImportDir(dir, build.ImportComment) + if err != nil { + log.Fatal(err) + } + return pkg +} + +// parseSymbol breaks str apart into a symbol and method. +// Both may be missing or the method may be missing. +// If present, each must be a valid Go identifier. +func parseSymbol(str string) (symbol, method string) { + if str == "" { + return + } + elem := strings.Split(str, ".") + switch len(elem) { + case 1: + case 2: + method = elem[1] + default: + log.Printf("too many periods in symbol specification") + usage() + } + symbol = elem[0] + return +} + +// isExported reports whether the name is an exported identifier. +// If the unexported flag (-u) is true, isExported returns true because +// it means that we treat the name as if it is exported. +func isExported(name string) bool { + return unexported || token.IsExported(name) +} + +// findNextPackage returns the next full file name path that matches the +// (perhaps partial) package path pkg. The boolean reports if any match was found. +func findNextPackage(pkg string) (string, bool) { + if filepath.IsAbs(pkg) { + if dirs.offset == 0 { + dirs.offset = -1 + return pkg, true + } + return "", false + } + if pkg == "" || token.IsExported(pkg) { // Upper case symbol cannot be a package name. + return "", false + } + pkg = path.Clean(pkg) + pkgSuffix := "/" + pkg + for { + d, ok := dirs.Next() + if !ok { + return "", false + } + if d.importPath == pkg || strings.HasSuffix(d.importPath, pkgSuffix) { + return d.dir, true + } + } +} + +var buildCtx = build.Default + +// splitGopath splits $GOPATH into a list of roots. +func splitGopath() []string { + return filepath.SplitList(buildCtx.GOPATH) +} |