| // 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) | |
| } |