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 // The first item in this list that succeeds is the one whose documentation
     16 // is printed. If there is a symbol but no package, the package in the current
     17 // directory is chosen.
     18 //
     19 // Two arguments:
     20 //	go doc <pkg> <sym>[.<method>]
     21 //
     22 // Show the documentation for the package, symbol, and method. The
     23 // first argument must be a full package path. This is similar to the
     24 // command-line usage for the godoc command.
     25 //
     26 // For commands, unless the -cmd flag is present "go doc command"
     27 // shows only the package-level docs for the package.
     28 //
     29 // For complete documentation, run "go help doc".
     30 package main
     31 
     32 import (
     33 	"flag"
     34 	"fmt"
     35 	"go/build"
     36 	"io"
     37 	"log"
     38 	"os"
     39 	"path"
     40 	"path/filepath"
     41 	"strings"
     42 	"unicode"
     43 	"unicode/utf8"
     44 )
     45 
     46 var (
     47 	unexported bool // -u flag
     48 	matchCase  bool // -c flag
     49 	showCmd    bool // -cmd flag
     50 )
     51 
     52 // usage is a replacement usage function for the flags package.
     53 func usage() {
     54 	fmt.Fprintf(os.Stderr, "Usage of [go] doc:\n")
     55 	fmt.Fprintf(os.Stderr, "\tgo doc\n")
     56 	fmt.Fprintf(os.Stderr, "\tgo doc <pkg>\n")
     57 	fmt.Fprintf(os.Stderr, "\tgo doc <sym>[.<method>]\n")
     58 	fmt.Fprintf(os.Stderr, "\tgo doc [<pkg>].<sym>[.<method>]\n")
     59 	fmt.Fprintf(os.Stderr, "\tgo doc <pkg> <sym>[.<method>]\n")
     60 	fmt.Fprintf(os.Stderr, "For more information run\n")
     61 	fmt.Fprintf(os.Stderr, "\tgo help doc\n\n")
     62 	fmt.Fprintf(os.Stderr, "Flags:\n")
     63 	flag.PrintDefaults()
     64 	os.Exit(2)
     65 }
     66 
     67 func main() {
     68 	log.SetFlags(0)
     69 	log.SetPrefix("doc: ")
     70 	err := do(os.Stdout, flag.CommandLine, os.Args[1:])
     71 	if err != nil {
     72 		log.Fatal(err)
     73 	}
     74 }
     75 
     76 // do is the workhorse, broken out of main to make testing easier.
     77 func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) {
     78 	flagSet.Usage = usage
     79 	unexported = false
     80 	matchCase = false
     81 	flagSet.BoolVar(&unexported, "u", false, "show unexported symbols as well as exported")
     82 	flagSet.BoolVar(&matchCase, "c", false, "symbol matching honors case (paths not affected)")
     83 	flagSet.BoolVar(&showCmd, "cmd", false, "show symbols with package docs even if package is a command")
     84 	flagSet.Parse(args)
     85 	buildPackage, userPath, symbol := parseArgs(flagSet.Args())
     86 	symbol, method := parseSymbol(symbol)
     87 	pkg := parsePackage(writer, buildPackage, userPath)
     88 	defer func() {
     89 		pkg.flush()
     90 		e := recover()
     91 		if e == nil {
     92 			return
     93 		}
     94 		pkgError, ok := e.(PackageError)
     95 		if ok {
     96 			err = pkgError
     97 			return
     98 		}
     99 		panic(e)
    100 	}()
    101 	switch {
    102 	case symbol == "":
    103 		pkg.packageDoc()
    104 		return
    105 	case method == "":
    106 		pkg.symbolDoc(symbol)
    107 	default:
    108 		pkg.methodDoc(symbol, method)
    109 	}
    110 	return nil
    111 }
    112 
    113 // parseArgs analyzes the arguments (if any) and returns the package
    114 // it represents, the part of the argument the user used to identify
    115 // the path (or "" if it's the current package) and the symbol
    116 // (possibly with a .method) within that package.
    117 // parseSymbol is used to analyze the symbol itself.
    118 func parseArgs(args []string) (*build.Package, string, string) {
    119 	switch len(args) {
    120 	default:
    121 		usage()
    122 	case 0:
    123 		// Easy: current directory.
    124 		return importDir(pwd()), "", ""
    125 	case 1:
    126 		// Done below.
    127 	case 2:
    128 		// Package must be importable.
    129 		pkg, err := build.Import(args[0], "", build.ImportComment)
    130 		if err != nil {
    131 			log.Fatalf("%s", err)
    132 		}
    133 		return pkg, args[0], args[1]
    134 	}
    135 	// Usual case: one argument.
    136 	arg := args[0]
    137 	// If it contains slashes, it begins with a package path.
    138 	// First, is it a complete package path as it is? If so, we are done.
    139 	// This avoids confusion over package paths that have other
    140 	// package paths as their prefix.
    141 	pkg, err := build.Import(arg, "", build.ImportComment)
    142 	if err == nil {
    143 		return pkg, arg, ""
    144 	}
    145 	// Another disambiguator: If the symbol starts with an upper
    146 	// case letter, it can only be a symbol in the current directory.
    147 	// Kills the problem caused by case-insensitive file systems
    148 	// matching an upper case name as a package name.
    149 	if isUpper(arg) {
    150 		pkg, err := build.ImportDir(".", build.ImportComment)
    151 		if err == nil {
    152 			return pkg, "", arg
    153 		}
    154 	}
    155 	// If it has a slash, it must be a package path but there is a symbol.
    156 	// It's the last package path we care about.
    157 	slash := strings.LastIndex(arg, "/")
    158 	// There may be periods in the package path before or after the slash
    159 	// and between a symbol and method.
    160 	// Split the string at various periods to see what we find.
    161 	// In general there may be ambiguities but this should almost always
    162 	// work.
    163 	var period int
    164 	// slash+1: if there's no slash, the value is -1 and start is 0; otherwise
    165 	// start is the byte after the slash.
    166 	for start := slash + 1; start < len(arg); start = period + 1 {
    167 		period = strings.Index(arg[start:], ".")
    168 		symbol := ""
    169 		if period < 0 {
    170 			period = len(arg)
    171 		} else {
    172 			period += start
    173 			symbol = arg[period+1:]
    174 		}
    175 		// Have we identified a package already?
    176 		pkg, err := build.Import(arg[0:period], "", build.ImportComment)
    177 		if err == nil {
    178 			return pkg, arg[0:period], symbol
    179 		}
    180 		// See if we have the basename or tail of a package, as in json for encoding/json
    181 		// or ivy/value for robpike.io/ivy/value.
    182 		path := findPackage(arg[0:period])
    183 		if path != "" {
    184 			return importDir(path), arg[0:period], symbol
    185 		}
    186 	}
    187 	// If it has a slash, we've failed.
    188 	if slash >= 0 {
    189 		log.Fatalf("no such package %s", arg[0:period])
    190 	}
    191 	// Guess it's a symbol in the current directory.
    192 	return importDir(pwd()), "", arg
    193 }
    194 
    195 // importDir is just an error-catching wrapper for build.ImportDir.
    196 func importDir(dir string) *build.Package {
    197 	pkg, err := build.ImportDir(dir, build.ImportComment)
    198 	if err != nil {
    199 		log.Fatal(err)
    200 	}
    201 	return pkg
    202 }
    203 
    204 // parseSymbol breaks str apart into a symbol and method.
    205 // Both may be missing or the method may be missing.
    206 // If present, each must be a valid Go identifier.
    207 func parseSymbol(str string) (symbol, method string) {
    208 	if str == "" {
    209 		return
    210 	}
    211 	elem := strings.Split(str, ".")
    212 	switch len(elem) {
    213 	case 1:
    214 	case 2:
    215 		method = elem[1]
    216 		isIdentifier(method)
    217 	default:
    218 		log.Printf("too many periods in symbol specification")
    219 		usage()
    220 	}
    221 	symbol = elem[0]
    222 	isIdentifier(symbol)
    223 	return
    224 }
    225 
    226 // isIdentifier checks that the name is valid Go identifier, and
    227 // logs and exits if it is not.
    228 func isIdentifier(name string) {
    229 	if len(name) == 0 {
    230 		log.Fatal("empty symbol")
    231 	}
    232 	for i, ch := range name {
    233 		if unicode.IsLetter(ch) || ch == '_' || i > 0 && unicode.IsDigit(ch) {
    234 			continue
    235 		}
    236 		log.Fatalf("invalid identifier %q", name)
    237 	}
    238 }
    239 
    240 // isExported reports whether the name is an exported identifier.
    241 // If the unexported flag (-u) is true, isExported returns true because
    242 // it means that we treat the name as if it is exported.
    243 func isExported(name string) bool {
    244 	return unexported || isUpper(name)
    245 }
    246 
    247 // isUpper reports whether the name starts with an upper case letter.
    248 func isUpper(name string) bool {
    249 	ch, _ := utf8.DecodeRuneInString(name)
    250 	return unicode.IsUpper(ch)
    251 }
    252 
    253 // findPackage returns the full file name path specified by the
    254 // (perhaps partial) package path pkg.
    255 func findPackage(pkg string) string {
    256 	if pkg == "" {
    257 		return ""
    258 	}
    259 	if isUpper(pkg) {
    260 		return "" // Upper case symbol cannot be a package name.
    261 	}
    262 	path := pathFor(build.Default.GOROOT, pkg)
    263 	if path != "" {
    264 		return path
    265 	}
    266 	for _, root := range splitGopath() {
    267 		path = pathFor(root, pkg)
    268 		if path != "" {
    269 			return path
    270 		}
    271 	}
    272 	return ""
    273 }
    274 
    275 // splitGopath splits $GOPATH into a list of roots.
    276 func splitGopath() []string {
    277 	return filepath.SplitList(build.Default.GOPATH)
    278 }
    279 
    280 // pathsFor recursively walks the tree at root looking for possible directories for the package:
    281 // those whose package path is pkg or which have a proper suffix pkg.
    282 func pathFor(root, pkg string) (result string) {
    283 	root = path.Join(root, "src")
    284 	slashDot := string(filepath.Separator) + "."
    285 	// We put a slash on the pkg so can use simple string comparison below
    286 	// yet avoid inadvertent matches, like /foobar matching bar.
    287 	pkgString := filepath.Clean(string(filepath.Separator) + pkg)
    288 
    289 	// We use panic/defer to short-circuit processing at the first match.
    290 	// A nil panic reports that the path has been found.
    291 	defer func() {
    292 		err := recover()
    293 		if err != nil {
    294 			panic(err)
    295 		}
    296 	}()
    297 
    298 	visit := func(pathName string, f os.FileInfo, err error) error {
    299 		if err != nil {
    300 			return nil
    301 		}
    302 		// One package per directory. Ignore the files themselves.
    303 		if !f.IsDir() {
    304 			return nil
    305 		}
    306 		// No .git or other dot nonsense please.
    307 		if strings.Contains(pathName, slashDot) {
    308 			return filepath.SkipDir
    309 		}
    310 		// Is the tail of the path correct?
    311 		if strings.HasSuffix(pathName, pkgString) && hasGoFiles(pathName) {
    312 			result = pathName
    313 			panic(nil)
    314 		}
    315 		return nil
    316 	}
    317 
    318 	filepath.Walk(root, visit)
    319 	return "" // Call to panic above sets the real value.
    320 }
    321 
    322 // hasGoFiles tests whether the directory contains at least one file with ".go"
    323 // extension
    324 func hasGoFiles(path string) bool {
    325 	dir, err := os.Open(path)
    326 	if err != nil {
    327 		// ignore unreadable directories
    328 		return false
    329 	}
    330 	defer dir.Close()
    331 
    332 	names, err := dir.Readdirnames(0)
    333 	if err != nil {
    334 		// ignore unreadable directories
    335 		return false
    336 	}
    337 
    338 	for _, name := range names {
    339 		if strings.HasSuffix(name, ".go") {
    340 			return true
    341 		}
    342 	}
    343 
    344 	return false
    345 }
    346 
    347 // pwd returns the current directory.
    348 func pwd() string {
    349 	wd, err := os.Getwd()
    350 	if err != nil {
    351 		log.Fatal(err)
    352 	}
    353 	return wd
    354 }
    355