github.com/xushiwei/go@v0.0.0-20130601165731-2b9d83f45bc9/src/cmd/godoc/main.go (about)

     1  // Copyright 2009 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  // godoc: Go Documentation Server
     6  
     7  // Web server tree:
     8  //
     9  //	http://godoc/		main landing page
    10  //	http://godoc/doc/	serve from $GOROOT/doc - spec, mem, etc.
    11  //	http://godoc/src/	serve files from $GOROOT/src; .go gets pretty-printed
    12  //	http://godoc/cmd/	serve documentation about commands
    13  //	http://godoc/pkg/	serve documentation about packages
    14  //				(idea is if you say import "compress/zlib", you go to
    15  //				http://godoc/pkg/compress/zlib)
    16  //
    17  // Command-line interface:
    18  //
    19  //	godoc packagepath [name ...]
    20  //
    21  //	godoc compress/zlib
    22  //		- prints doc for package compress/zlib
    23  //	godoc crypto/block Cipher NewCMAC
    24  //		- prints doc for Cipher and NewCMAC in package crypto/block
    25  
    26  // +build !appengine
    27  
    28  package main
    29  
    30  import (
    31  	"archive/zip"
    32  	"bytes"
    33  	"errors"
    34  	_ "expvar" // to serve /debug/vars
    35  	"flag"
    36  	"fmt"
    37  	"go/ast"
    38  	"go/build"
    39  	"go/printer"
    40  	"io"
    41  	"log"
    42  	"net/http"
    43  	_ "net/http/pprof" // to serve /debug/pprof/*
    44  	"net/url"
    45  	"os"
    46  	pathpkg "path"
    47  	"path/filepath"
    48  	"regexp"
    49  	"runtime"
    50  	"strings"
    51  )
    52  
    53  const defaultAddr = ":6060" // default webserver address
    54  
    55  var (
    56  	// file system to serve
    57  	// (with e.g.: zip -r go.zip $GOROOT -i \*.go -i \*.html -i \*.css -i \*.js -i \*.txt -i \*.c -i \*.h -i \*.s -i \*.png -i \*.jpg -i \*.sh -i favicon.ico)
    58  	zipfile = flag.String("zip", "", "zip file providing the file system to serve; disabled if empty")
    59  
    60  	// file-based index
    61  	writeIndex = flag.Bool("write_index", false, "write index to a file; the file name must be specified with -index_files")
    62  
    63  	// network
    64  	httpAddr   = flag.String("http", "", "HTTP service address (e.g., '"+defaultAddr+"')")
    65  	serverAddr = flag.String("server", "", "webserver address for command line searches")
    66  
    67  	// layout control
    68  	html    = flag.Bool("html", false, "print HTML in command-line mode")
    69  	srcMode = flag.Bool("src", false, "print (exported) source in command-line mode")
    70  	urlFlag = flag.String("url", "", "print HTML for named URL")
    71  
    72  	// command-line searches
    73  	query = flag.Bool("q", false, "arguments are considered search queries")
    74  )
    75  
    76  func serveError(w http.ResponseWriter, r *http.Request, relpath string, err error) {
    77  	w.WriteHeader(http.StatusNotFound)
    78  	servePage(w, Page{
    79  		Title:    "File " + relpath,
    80  		Subtitle: relpath,
    81  		Body:     applyTemplate(errorHTML, "errorHTML", err), // err may contain an absolute path!
    82  	})
    83  }
    84  
    85  func usage() {
    86  	fmt.Fprintf(os.Stderr,
    87  		"usage: godoc package [name ...]\n"+
    88  			"	godoc -http="+defaultAddr+"\n")
    89  	flag.PrintDefaults()
    90  	os.Exit(2)
    91  }
    92  
    93  func loggingHandler(h http.Handler) http.Handler {
    94  	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
    95  		log.Printf("%s\t%s", req.RemoteAddr, req.URL)
    96  		h.ServeHTTP(w, req)
    97  	})
    98  }
    99  
   100  func remoteSearch(query string) (res *http.Response, err error) {
   101  	// list of addresses to try
   102  	var addrs []string
   103  	if *serverAddr != "" {
   104  		// explicit server address - only try this one
   105  		addrs = []string{*serverAddr}
   106  	} else {
   107  		addrs = []string{
   108  			defaultAddr,
   109  			"golang.org",
   110  		}
   111  	}
   112  
   113  	// remote search
   114  	search := remoteSearchURL(query, *html)
   115  	for _, addr := range addrs {
   116  		url := "http://" + addr + search
   117  		res, err = http.Get(url)
   118  		if err == nil && res.StatusCode == http.StatusOK {
   119  			break
   120  		}
   121  	}
   122  
   123  	if err == nil && res.StatusCode != http.StatusOK {
   124  		err = errors.New(res.Status)
   125  	}
   126  
   127  	return
   128  }
   129  
   130  // Does s look like a regular expression?
   131  func isRegexp(s string) bool {
   132  	return strings.IndexAny(s, ".(|)*+?^$[]") >= 0
   133  }
   134  
   135  // Make a regular expression of the form
   136  // names[0]|names[1]|...names[len(names)-1].
   137  // Returns nil if the regular expression is illegal.
   138  func makeRx(names []string) (rx *regexp.Regexp) {
   139  	if len(names) > 0 {
   140  		s := ""
   141  		for i, name := range names {
   142  			if i > 0 {
   143  				s += "|"
   144  			}
   145  			if isRegexp(name) {
   146  				s += name
   147  			} else {
   148  				s += "^" + name + "$" // must match exactly
   149  			}
   150  		}
   151  		rx, _ = regexp.Compile(s) // rx is nil if there's a compilation error
   152  	}
   153  	return
   154  }
   155  
   156  func main() {
   157  	flag.Usage = usage
   158  	flag.Parse()
   159  
   160  	// Check usage: either server and no args, command line and args, or index creation mode
   161  	if (*httpAddr != "" || *urlFlag != "") != (flag.NArg() == 0) && !*writeIndex {
   162  		usage()
   163  	}
   164  
   165  	if *tabwidth < 0 {
   166  		log.Fatalf("negative tabwidth %d", *tabwidth)
   167  	}
   168  
   169  	// Determine file system to use.
   170  	// TODO(gri) - fs and fsHttp should really be the same. Try to unify.
   171  	//           - fsHttp doesn't need to be set up in command-line mode,
   172  	//             same is true for the http handlers in initHandlers.
   173  	if *zipfile == "" {
   174  		// use file system of underlying OS
   175  		fs.Bind("/", OS(*goroot), "/", bindReplace)
   176  		if *templateDir != "" {
   177  			fs.Bind("/lib/godoc", OS(*templateDir), "/", bindBefore)
   178  		}
   179  	} else {
   180  		// use file system specified via .zip file (path separator must be '/')
   181  		rc, err := zip.OpenReader(*zipfile)
   182  		if err != nil {
   183  			log.Fatalf("%s: %s\n", *zipfile, err)
   184  		}
   185  		defer rc.Close() // be nice (e.g., -writeIndex mode)
   186  		fs.Bind("/", NewZipFS(rc, *zipfile), *goroot, bindReplace)
   187  	}
   188  
   189  	// Bind $GOPATH trees into Go root.
   190  	for _, p := range filepath.SplitList(build.Default.GOPATH) {
   191  		fs.Bind("/src/pkg", OS(p), "/src", bindAfter)
   192  	}
   193  
   194  	readTemplates()
   195  	initHandlers()
   196  
   197  	if *writeIndex {
   198  		// Write search index and exit.
   199  		if *indexFiles == "" {
   200  			log.Fatal("no index file specified")
   201  		}
   202  
   203  		log.Println("initialize file systems")
   204  		*verbose = true // want to see what happens
   205  		initFSTree()
   206  
   207  		*indexThrottle = 1
   208  		updateIndex()
   209  
   210  		log.Println("writing index file", *indexFiles)
   211  		f, err := os.Create(*indexFiles)
   212  		if err != nil {
   213  			log.Fatal(err)
   214  		}
   215  		index, _ := searchIndex.get()
   216  		err = index.(*Index).Write(f)
   217  		if err != nil {
   218  			log.Fatal(err)
   219  		}
   220  
   221  		log.Println("done")
   222  		return
   223  	}
   224  
   225  	// Print content that would be served at the URL *urlFlag.
   226  	if *urlFlag != "" {
   227  		registerPublicHandlers(http.DefaultServeMux)
   228  		initFSTree()
   229  		updateMetadata()
   230  		// Try up to 10 fetches, following redirects.
   231  		urlstr := *urlFlag
   232  		for i := 0; i < 10; i++ {
   233  			// Prepare request.
   234  			u, err := url.Parse(urlstr)
   235  			if err != nil {
   236  				log.Fatal(err)
   237  			}
   238  			req := &http.Request{
   239  				URL: u,
   240  			}
   241  
   242  			// Invoke default HTTP handler to serve request
   243  			// to our buffering httpWriter.
   244  			w := &httpWriter{h: http.Header{}, code: 200}
   245  			http.DefaultServeMux.ServeHTTP(w, req)
   246  
   247  			// Return data, error, or follow redirect.
   248  			switch w.code {
   249  			case 200: // ok
   250  				os.Stdout.Write(w.Bytes())
   251  				return
   252  			case 301, 302, 303, 307: // redirect
   253  				redirect := w.h.Get("Location")
   254  				if redirect == "" {
   255  					log.Fatalf("HTTP %d without Location header", w.code)
   256  				}
   257  				urlstr = redirect
   258  			default:
   259  				log.Fatalf("HTTP error %d", w.code)
   260  			}
   261  		}
   262  		log.Fatalf("too many redirects")
   263  	}
   264  
   265  	if *httpAddr != "" {
   266  		// HTTP server mode.
   267  		var handler http.Handler = http.DefaultServeMux
   268  		if *verbose {
   269  			log.Printf("Go Documentation Server")
   270  			log.Printf("version = %s", runtime.Version())
   271  			log.Printf("address = %s", *httpAddr)
   272  			log.Printf("goroot = %s", *goroot)
   273  			log.Printf("tabwidth = %d", *tabwidth)
   274  			switch {
   275  			case !*indexEnabled:
   276  				log.Print("search index disabled")
   277  			case *maxResults > 0:
   278  				log.Printf("full text index enabled (maxresults = %d)", *maxResults)
   279  			default:
   280  				log.Print("identifier search index enabled")
   281  			}
   282  			fs.Fprint(os.Stderr)
   283  			handler = loggingHandler(handler)
   284  		}
   285  
   286  		registerPublicHandlers(http.DefaultServeMux)
   287  		registerPlaygroundHandlers(http.DefaultServeMux)
   288  
   289  		// Initialize default directory tree with corresponding timestamp.
   290  		// (Do it in a goroutine so that launch is quick.)
   291  		go initFSTree()
   292  
   293  		// Immediately update metadata.
   294  		updateMetadata()
   295  		// Periodically refresh metadata.
   296  		go refreshMetadataLoop()
   297  
   298  		// Initialize search index.
   299  		if *indexEnabled {
   300  			go indexer()
   301  		}
   302  
   303  		// Start http server.
   304  		if err := http.ListenAndServe(*httpAddr, handler); err != nil {
   305  			log.Fatalf("ListenAndServe %s: %v", *httpAddr, err)
   306  		}
   307  
   308  		return
   309  	}
   310  
   311  	// Command line mode.
   312  	if *html {
   313  		packageText = packageHTML
   314  		searchText = packageHTML
   315  	}
   316  
   317  	if *query {
   318  		// Command-line queries.
   319  		for i := 0; i < flag.NArg(); i++ {
   320  			res, err := remoteSearch(flag.Arg(i))
   321  			if err != nil {
   322  				log.Fatalf("remoteSearch: %s", err)
   323  			}
   324  			io.Copy(os.Stdout, res.Body)
   325  		}
   326  		return
   327  	}
   328  
   329  	// Determine paths.
   330  	//
   331  	// If we are passed an operating system path like . or ./foo or /foo/bar or c:\mysrc,
   332  	// we need to map that path somewhere in the fs name space so that routines
   333  	// like getPageInfo will see it.  We use the arbitrarily-chosen virtual path "/target"
   334  	// for this.  That is, if we get passed a directory like the above, we map that
   335  	// directory so that getPageInfo sees it as /target.
   336  	const target = "/target"
   337  	const cmdPrefix = "cmd/"
   338  	path := flag.Arg(0)
   339  	var forceCmd bool
   340  	var abspath, relpath string
   341  	if filepath.IsAbs(path) {
   342  		fs.Bind(target, OS(path), "/", bindReplace)
   343  		abspath = target
   344  	} else if build.IsLocalImport(path) {
   345  		cwd, _ := os.Getwd() // ignore errors
   346  		path = filepath.Join(cwd, path)
   347  		fs.Bind(target, OS(path), "/", bindReplace)
   348  		abspath = target
   349  	} else if strings.HasPrefix(path, cmdPrefix) {
   350  		path = strings.TrimPrefix(path, cmdPrefix)
   351  		forceCmd = true
   352  	} else if bp, _ := build.Import(path, "", build.FindOnly); bp.Dir != "" && bp.ImportPath != "" {
   353  		fs.Bind(target, OS(bp.Dir), "/", bindReplace)
   354  		abspath = target
   355  		relpath = bp.ImportPath
   356  	} else {
   357  		abspath = pathpkg.Join(pkgHandler.fsRoot, path)
   358  	}
   359  	if relpath == "" {
   360  		relpath = abspath
   361  	}
   362  
   363  	var mode PageInfoMode
   364  	if relpath == builtinPkgPath {
   365  		// the fake built-in package contains unexported identifiers
   366  		mode = noFiltering
   367  	}
   368  	if *srcMode {
   369  		// only filter exports if we don't have explicit command-line filter arguments
   370  		if flag.NArg() > 1 {
   371  			mode |= noFiltering
   372  		}
   373  		mode |= showSource
   374  	}
   375  
   376  	// first, try as package unless forced as command
   377  	var info *PageInfo
   378  	if !forceCmd {
   379  		info = pkgHandler.getPageInfo(abspath, relpath, mode)
   380  	}
   381  
   382  	// second, try as command unless the path is absolute
   383  	// (the go command invokes godoc w/ absolute paths; don't override)
   384  	var cinfo *PageInfo
   385  	if !filepath.IsAbs(path) {
   386  		abspath = pathpkg.Join(cmdHandler.fsRoot, path)
   387  		cinfo = cmdHandler.getPageInfo(abspath, relpath, mode)
   388  	}
   389  
   390  	// determine what to use
   391  	if info == nil || info.IsEmpty() {
   392  		if cinfo != nil && !cinfo.IsEmpty() {
   393  			// only cinfo exists - switch to cinfo
   394  			info = cinfo
   395  		}
   396  	} else if cinfo != nil && !cinfo.IsEmpty() {
   397  		// both info and cinfo exist - use cinfo if info
   398  		// contains only subdirectory information
   399  		if info.PAst == nil && info.PDoc == nil {
   400  			info = cinfo
   401  		} else {
   402  			fmt.Printf("use 'godoc %s%s' for documentation on the %s command \n\n", cmdPrefix, relpath, relpath)
   403  		}
   404  	}
   405  
   406  	if info == nil {
   407  		log.Fatalf("%s: no such directory or package", flag.Arg(0))
   408  	}
   409  	if info.Err != nil {
   410  		log.Fatalf("%v", info.Err)
   411  	}
   412  
   413  	if info.PDoc != nil && info.PDoc.ImportPath == target {
   414  		// Replace virtual /target with actual argument from command line.
   415  		info.PDoc.ImportPath = flag.Arg(0)
   416  	}
   417  
   418  	// If we have more than one argument, use the remaining arguments for filtering.
   419  	if flag.NArg() > 1 {
   420  		args := flag.Args()[1:]
   421  		rx := makeRx(args)
   422  		if rx == nil {
   423  			log.Fatalf("illegal regular expression from %v", args)
   424  		}
   425  
   426  		filter := func(s string) bool { return rx.MatchString(s) }
   427  		switch {
   428  		case info.PAst != nil:
   429  			cmap := ast.NewCommentMap(info.FSet, info.PAst, info.PAst.Comments)
   430  			ast.FilterFile(info.PAst, filter)
   431  			// Special case: Don't use templates for printing
   432  			// so we only get the filtered declarations without
   433  			// package clause or extra whitespace.
   434  			for i, d := range info.PAst.Decls {
   435  				// determine the comments associated with d only
   436  				comments := cmap.Filter(d).Comments()
   437  				cn := &printer.CommentedNode{Node: d, Comments: comments}
   438  				if i > 0 {
   439  					fmt.Println()
   440  				}
   441  				if *html {
   442  					var buf bytes.Buffer
   443  					writeNode(&buf, info.FSet, cn)
   444  					FormatText(os.Stdout, buf.Bytes(), -1, true, "", nil)
   445  				} else {
   446  					writeNode(os.Stdout, info.FSet, cn)
   447  				}
   448  				fmt.Println()
   449  			}
   450  			return
   451  
   452  		case info.PDoc != nil:
   453  			info.PDoc.Filter(filter)
   454  		}
   455  	}
   456  
   457  	if err := packageText.Execute(os.Stdout, info); err != nil {
   458  		log.Printf("packageText.Execute: %s", err)
   459  	}
   460  }
   461  
   462  // An httpWriter is an http.ResponseWriter writing to a bytes.Buffer.
   463  type httpWriter struct {
   464  	bytes.Buffer
   465  	h    http.Header
   466  	code int
   467  }
   468  
   469  func (w *httpWriter) Header() http.Header  { return w.h }
   470  func (w *httpWriter) WriteHeader(code int) { w.code = code }