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