Home | History | Annotate | Download | only in 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>[.<method>]
     14 //	go doc [<pkg>.]<sym>[.<method>]
     15 //	go doc [<pkg>.][<sym>.]<method>
     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>[.<method>]
     23 //
     24 // Show the documentation for the package, symbol, and method. 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 // For complete documentation, run "go help doc".
     32 package main
     33 
     34 import (
     35 	"bytes"
     36 	"flag"
     37 	"fmt"
     38 	"go/build"
     39 	"io"
     40 	"log"
     41 	"os"
     42 	"path/filepath"
     43 	"strings"
     44 	"unicode"
     45 	"unicode/utf8"
     46 )
     47 
     48 var (
     49 	unexported bool // -u flag
     50 	matchCase  bool // -c flag
     51 	showCmd    bool // -cmd flag
     52 )
     53 
     54 // usage is a replacement usage function for the flags package.
     55 func usage() {
     56 	fmt.Fprintf(os.Stderr, "Usage of [go] doc:\n")
     57 	fmt.Fprintf(os.Stderr, "\tgo doc\n")
     58 	fmt.Fprintf(os.Stderr, "\tgo doc <pkg>\n")
     59 	fmt.Fprintf(os.Stderr, "\tgo doc <sym>[.<method>]\n")
     60 	fmt.Fprintf(os.Stderr, "\tgo doc [<pkg>].<sym>[.<method>]\n")
     61 	fmt.Fprintf(os.Stderr, "\tgo doc <pkg> <sym>[.<method>]\n")
     62 	fmt.Fprintf(os.Stderr, "For more information run\n")
     63 	fmt.Fprintf(os.Stderr, "\tgo help doc\n\n")
     64 	fmt.Fprintf(os.Stderr, "Flags:\n")
     65 	flag.PrintDefaults()
     66 	os.Exit(2)
     67 }
     68 
     69 func main() {
     70 	log.SetFlags(0)
     71 	log.SetPrefix("doc: ")
     72 	err := do(os.Stdout, flag.CommandLine, os.Args[1:])
     73 	if err != nil {
     74 		log.Fatal(err)
     75 	}
     76 }
     77 
     78 // do is the workhorse, broken out of main to make testing easier.
     79 func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) {
     80 	flagSet.Usage = usage
     81 	unexported = false
     82 	matchCase = false
     83 	flagSet.BoolVar(&unexported, "u", false, "show unexported symbols as well as exported")
     84 	flagSet.BoolVar(&matchCase, "c", false, "symbol matching honors case (paths not affected)")
     85 	flagSet.BoolVar(&showCmd, "cmd", false, "show symbols with package docs even if package is a command")
     86 	flagSet.Parse(args)
     87 	var paths []string
     88 	var symbol, method string
     89 	// Loop until something is printed.
     90 	dirs.Reset()
     91 	for i := 0; ; i++ {
     92 		buildPackage, userPath, sym, more := parseArgs(flagSet.Args())
     93 		if i > 0 && !more { // Ignore the "more" bit on the first iteration.
     94 			return failMessage(paths, symbol, method)
     95 		}
     96 		symbol, method = parseSymbol(sym)
     97 		pkg := parsePackage(writer, buildPackage, userPath)
     98 		paths = append(paths, pkg.prettyPath())
     99 
    100 		defer func() {
    101 			pkg.flush()
    102 			e := recover()
    103 			if e == nil {
    104 				return
    105 			}
    106 			pkgError, ok := e.(PackageError)
    107 			if ok {
    108 				err = pkgError
    109 				return
    110 			}
    111 			panic(e)
    112 		}()
    113 
    114 		// The builtin package needs special treatment: its symbols are lower
    115 		// case but we want to see them, always.
    116 		if pkg.build.ImportPath == "builtin" {
    117 			unexported = true
    118 		}
    119 
    120 		switch {
    121 		case symbol == "":
    122 			pkg.packageDoc() // The package exists, so we got some output.
    123 			return
    124 		case method == "":
    125 			if pkg.symbolDoc(symbol) {
    126 				return
    127 			}
    128 		default:
    129 			if pkg.methodDoc(symbol, method) {
    130 				return
    131 			}
    132 		}
    133 	}
    134 }
    135 
    136 // failMessage creates a nicely formatted error message when there is no result to show.
    137 func failMessage(paths []string, symbol, method string) error {
    138 	var b bytes.Buffer
    139 	if len(paths) > 1 {
    140 		b.WriteString("s")
    141 	}
    142 	b.WriteString(" ")
    143 	for i, path := range paths {
    144 		if i > 0 {
    145 			b.WriteString(", ")
    146 		}
    147 		b.WriteString(path)
    148 	}
    149 	if method == "" {
    150 		return fmt.Errorf("no symbol %s in package%s", symbol, &b)
    151 	}
    152 	return fmt.Errorf("no method %s.%s in package%s", symbol, method, &b)
    153 }
    154 
    155 // parseArgs analyzes the arguments (if any) and returns the package
    156 // it represents, the part of the argument the user used to identify
    157 // the path (or "" if it's the current package) and the symbol
    158 // (possibly with a .method) within that package.
    159 // parseSymbol is used to analyze the symbol itself.
    160 // The boolean final argument reports whether it is possible that
    161 // there may be more directories worth looking at. It will only
    162 // be true if the package path is a partial match for some directory
    163 // and there may be more matches. For example, if the argument
    164 // is rand.Float64, we must scan both crypto/rand and math/rand
    165 // to find the symbol, and the first call will return crypto/rand, true.
    166 func parseArgs(args []string) (pkg *build.Package, path, symbol string, more bool) {
    167 	switch len(args) {
    168 	default:
    169 		usage()
    170 	case 0:
    171 		// Easy: current directory.
    172 		return importDir(pwd()), "", "", false
    173 	case 1:
    174 		// Done below.
    175 	case 2:
    176 		// Package must be importable.
    177 		pkg, err := build.Import(args[0], "", build.ImportComment)
    178 		if err != nil {
    179 			log.Fatalf("%s", err)
    180 		}
    181 		return pkg, args[0], args[1], false
    182 	}
    183 	// Usual case: one argument.
    184 	arg := args[0]
    185 	// If it contains slashes, it begins with a package path.
    186 	// First, is it a complete package path as it is? If so, we are done.
    187 	// This avoids confusion over package paths that have other
    188 	// package paths as their prefix.
    189 	pkg, err := build.Import(arg, "", build.ImportComment)
    190 	if err == nil {
    191 		return pkg, arg, "", false
    192 	}
    193 	// Another disambiguator: If the symbol starts with an upper
    194 	// case letter, it can only be a symbol in the current directory.
    195 	// Kills the problem caused by case-insensitive file systems
    196 	// matching an upper case name as a package name.
    197 	if isUpper(arg) {
    198 		pkg, err := build.ImportDir(".", build.ImportComment)
    199 		if err == nil {
    200 			return pkg, "", arg, false
    201 		}
    202 	}
    203 	// If it has a slash, it must be a package path but there is a symbol.
    204 	// It's the last package path we care about.
    205 	slash := strings.LastIndex(arg, "/")
    206 	// There may be periods in the package path before or after the slash
    207 	// and between a symbol and method.
    208 	// Split the string at various periods to see what we find.
    209 	// In general there may be ambiguities but this should almost always
    210 	// work.
    211 	var period int
    212 	// slash+1: if there's no slash, the value is -1 and start is 0; otherwise
    213 	// start is the byte after the slash.
    214 	for start := slash + 1; start < len(arg); start = period + 1 {
    215 		period = strings.Index(arg[start:], ".")
    216 		symbol := ""
    217 		if period < 0 {
    218 			period = len(arg)
    219 		} else {
    220 			period += start
    221 			symbol = arg[period+1:]
    222 		}
    223 		// Have we identified a package already?
    224 		pkg, err := build.Import(arg[0:period], "", build.ImportComment)
    225 		if err == nil {
    226 			return pkg, arg[0:period], symbol, false
    227 		}
    228 		// See if we have the basename or tail of a package, as in json for encoding/json
    229 		// or ivy/value for robpike.io/ivy/value.
    230 		// Launch findPackage as a goroutine so it can return multiple paths if required.
    231 		path, ok := findPackage(arg[0:period])
    232 		if ok {
    233 			return importDir(path), arg[0:period], symbol, true
    234 		}
    235 		dirs.Reset() // Next iteration of for loop must scan all the directories again.
    236 	}
    237 	// If it has a slash, we've failed.
    238 	if slash >= 0 {
    239 		log.Fatalf("no such package %s", arg[0:period])
    240 	}
    241 	// Guess it's a symbol in the current directory.
    242 	return importDir(pwd()), "", arg, false
    243 }
    244 
    245 // importDir is just an error-catching wrapper for build.ImportDir.
    246 func importDir(dir string) *build.Package {
    247 	pkg, err := build.ImportDir(dir, build.ImportComment)
    248 	if err != nil {
    249 		log.Fatal(err)
    250 	}
    251 	return pkg
    252 }
    253 
    254 // parseSymbol breaks str apart into a symbol and method.
    255 // Both may be missing or the method may be missing.
    256 // If present, each must be a valid Go identifier.
    257 func parseSymbol(str string) (symbol, method string) {
    258 	if str == "" {
    259 		return
    260 	}
    261 	elem := strings.Split(str, ".")
    262 	switch len(elem) {
    263 	case 1:
    264 	case 2:
    265 		method = elem[1]
    266 		isIdentifier(method)
    267 	default:
    268 		log.Printf("too many periods in symbol specification")
    269 		usage()
    270 	}
    271 	symbol = elem[0]
    272 	isIdentifier(symbol)
    273 	return
    274 }
    275 
    276 // isIdentifier checks that the name is valid Go identifier, and
    277 // logs and exits if it is not.
    278 func isIdentifier(name string) {
    279 	if len(name) == 0 {
    280 		log.Fatal("empty symbol")
    281 	}
    282 	for i, ch := range name {
    283 		if unicode.IsLetter(ch) || ch == '_' || i > 0 && unicode.IsDigit(ch) {
    284 			continue
    285 		}
    286 		log.Fatalf("invalid identifier %q", name)
    287 	}
    288 }
    289 
    290 // isExported reports whether the name is an exported identifier.
    291 // If the unexported flag (-u) is true, isExported returns true because
    292 // it means that we treat the name as if it is exported.
    293 func isExported(name string) bool {
    294 	return unexported || isUpper(name)
    295 }
    296 
    297 // isUpper reports whether the name starts with an upper case letter.
    298 func isUpper(name string) bool {
    299 	ch, _ := utf8.DecodeRuneInString(name)
    300 	return unicode.IsUpper(ch)
    301 }
    302 
    303 // findPackage returns the full file name path that first matches the
    304 // (perhaps partial) package path pkg. The boolean reports if any match was found.
    305 func findPackage(pkg string) (string, bool) {
    306 	if pkg == "" || isUpper(pkg) { // Upper case symbol cannot be a package name.
    307 		return "", false
    308 	}
    309 	pkgString := filepath.Clean(string(filepath.Separator) + pkg)
    310 	for {
    311 		path, ok := dirs.Next()
    312 		if !ok {
    313 			return "", false
    314 		}
    315 		if strings.HasSuffix(path, pkgString) {
    316 			return path, true
    317 		}
    318 	}
    319 }
    320 
    321 // splitGopath splits $GOPATH into a list of roots.
    322 func splitGopath() []string {
    323 	return filepath.SplitList(build.Default.GOPATH)
    324 }
    325 
    326 // pwd returns the current directory.
    327 func pwd() string {
    328 	wd, err := os.Getwd()
    329 	if err != nil {
    330 		log.Fatal(err)
    331 	}
    332 	return wd
    333 }
    334