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