From: Robert Griesemer Subdirectories
+ {.repeated section @}
+ {Name|html}
+ {.end}
+
+{.end}
+{.section PDoc}
+ package {PackageName|html}
+ import "{ImportPath|html}"Constants
+ {.repeated section @}
+ {Doc|html-comment}
+ {Decl|html}
+ {.end}
+ {.end}
+ {.section Vars}
+
+ Variables
+ {.repeated section @}
+ {Doc|html-comment}
+ {Decl|html}
+ {.end}
+ {.end}
+ {.section Funcs}
+
+ {.repeated section @}
+ func {Name|html}
+ {Decl|html}
+ type {.section Type}{Name|html}{.end}
+ {Doc|html-comment}
+ {Decl|html}
{Decl|html}
{Decl|html}
+{.repeated section list}
+{src}{.section msg}«{msg|html}»{.end}{.end}
diff --git a/lib/godoc/parseerror.txt b/lib/godoc/parseerror.txt
new file mode 100644
index 0000000000..609cb511b5
--- /dev/null
+++ b/lib/godoc/parseerror.txt
@@ -0,0 +1,6 @@
+parse errors:
+{.repeated section list}
+{.section msg}
+{filename}:{line}: {msg}
+{.end}
+{.end}
diff --git a/src/cmd/godoc/Makefile b/src/cmd/godoc/Makefile
new file mode 100644
index 0000000000..8365f9feac
--- /dev/null
+++ b/src/cmd/godoc/Makefile
@@ -0,0 +1,21 @@
+# Copyright 2009 The Go Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+include $(GOROOT)/src/Make.$(GOARCH)
+
+TARG=godoc
+OFILES=\
+ godoc.$O\
+
+$(TARG): $(OFILES)
+ $(LD) -o $(TARG) $(OFILES)
+
+clean:
+ rm -f $(OFILES) $(TARG)
+
+install: $(TARG)
+ cp $(TARG) $(HOME)/bin/$(TARG)
+
+%.$O: %.go
+ $(GC) $<
diff --git a/src/cmd/godoc/godoc.go b/src/cmd/godoc/godoc.go
new file mode 100644
index 0000000000..971ef4425b
--- /dev/null
+++ b/src/cmd/godoc/godoc.go
@@ -0,0 +1,707 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// godoc: Go Documentation Server
+
+// Web server tree:
+//
+// http://godoc/ main landing page
+// http://godoc/doc/ serve from $GOROOT/doc - spec, mem, tutorial, etc.
+// http://godoc/src/ serve files from $GOROOT/src; .go gets pretty-printed
+// http://godoc/cmd/ serve documentation about commands (TODO)
+// http://godoc/pkg/ serve documentation about packages
+// (idea is if you say import "compress/zlib", you go to
+// http://godoc/pkg/compress/zlib)
+//
+// Command-line interface:
+//
+// godoc packagepath [name ...]
+//
+// godoc compress/zlib
+// - prints doc for package compress/zlib
+// godoc crypto/block Cipher NewCMAC
+// - prints doc for Cipher and NewCMAC in package crypto/block
+
+
+package main
+
+import (
+ "container/vector";
+ "flag";
+ "fmt";
+ "go/ast";
+ "go/doc";
+ "go/parser";
+ "go/printer";
+ "go/token";
+ "http";
+ "io";
+ "log";
+ "net";
+ "os";
+ pathutil "path";
+ "sort";
+ "strings";
+ "sync";
+ "syscall";
+ "tabwriter";
+ "template";
+ "time";
+)
+
+
+const Pkg = "/pkg/" // name for auto-generated package documentation tree
+
+
+type timeStamp struct {
+ mutex sync.RWMutex;
+ seconds int64;
+}
+
+
+func (ts *timeStamp) set() {
+ ts.mutex.Lock();
+ ts.seconds = time.Seconds();
+ ts.mutex.Unlock();
+}
+
+
+func (ts *timeStamp) get() int64 {
+ ts.mutex.RLock();
+ defer ts.mutex.RUnlock();
+ return ts.seconds;
+}
+
+
+var (
+ verbose = flag.Bool("v", false, "verbose mode");
+
+ // file system roots
+ goroot string;
+ pkgroot = flag.String("pkgroot", "src/pkg", "root package source directory (if unrooted, relative to goroot)");
+ tmplroot = flag.String("tmplroot", "src/cmd/godoc", "root template directory (if unrooted, relative to goroot)");
+
+ // periodic sync
+ syncCmd = flag.String("sync", "", "sync command; disabled if empty");
+ syncMin = flag.Int("sync_minutes", 0, "sync interval in minutes; disabled if <= 0");
+ syncTime timeStamp; // time of last p4 sync
+
+ // layout control
+ tabwidth = flag.Int("tabwidth", 4, "tab width");
+ html = flag.Bool("html", false, "print HTML in command-line mode");
+
+ // server control
+ httpaddr = flag.String("http", "", "HTTP service address (e.g., ':6060')");
+)
+
+
+func init() {
+ var err os.Error;
+ goroot, err = os.Getenv("GOROOT");
+ if err != nil {
+ goroot = "/home/r/go-release/go";
+ }
+ flag.StringVar(&goroot, "goroot", goroot, "Go root directory");
+ syncTime.set(); // have a reasonable initial value
+}
+
+
+// ----------------------------------------------------------------------------
+// Support
+
+func isDir(name string) bool {
+ d, err := os.Stat(name);
+ return err == nil && d.IsDirectory();
+}
+
+
+func isGoFile(dir *os.Dir) bool {
+ return dir.IsRegular() && pathutil.Ext(dir.Name) == ".go";
+}
+
+
+func isPkgDir(dir *os.Dir) bool {
+ return dir.IsDirectory() && dir.Name != "_obj";
+}
+
+
+func makeTabwriter(writer io.Writer) *tabwriter.Writer {
+ return tabwriter.NewWriter(writer, *tabwidth, 1, byte(' '), 0);
+}
+
+
+// ----------------------------------------------------------------------------
+// Parsing
+
+// A single error in the parsed file.
+type parseError struct {
+ src []byte; // source before error
+ line int; // line number of error
+ msg string; // error message
+}
+
+
+// All the errors in the parsed file, plus surrounding source code.
+// Each error has a slice giving the source text preceding it
+// (starting where the last error occurred). The final element in list[]
+// has msg = "", to give the remainder of the source code.
+// This data structure is handed to the templates parseerror.txt and parseerror.html.
+//
+type parseErrors struct {
+ filename string; // path to file
+ list []parseError; // the errors
+ src []byte; // the file's entire source code
+}
+
+
+// Parses a file (path) and returns the corresponding AST and
+// a sorted list (by file position) of errors, if any.
+//
+func parse(path string, mode uint) (*ast.Program, *parseErrors) {
+ src, err := io.ReadFile(path);
+ if err != nil {
+ log.Stderrf("ReadFile %s: %v", path, err);
+ errs := []parseError{parseError{nil, 0, err.String()}};
+ return nil, &parseErrors{path, errs, nil};
+ }
+
+ prog, err := parser.Parse(src, mode);
+ if err != nil {
+ // sort and convert error list
+ if errors, ok := err.(parser.ErrorList); ok {
+ sort.Sort(errors);
+ errs := make([]parseError, len(errors) + 1); // +1 for final fragment of source
+ offs := 0;
+ for i, r := range errors {
+ // Should always be true, but check for robustness.
+ if 0 <= r.Pos.Offset && r.Pos.Offset <= len(src) {
+ errs[i].src = src[offs : r.Pos.Offset];
+ offs = r.Pos.Offset;
+ }
+ errs[i].line = r.Pos.Line;
+ errs[i].msg = r.Msg;
+ }
+ errs[len(errors)].src = src[offs : len(src)];
+ return nil, &parseErrors{path, errs, src};
+ } else {
+ // TODO should have some default handling here to be more robust
+ panic("unreachable");
+ }
+ }
+
+ return prog, nil;
+}
+
+
+// ----------------------------------------------------------------------------
+// Templates
+
+// Return text for an AST node.
+func nodeText(node interface{}, mode uint) []byte {
+ var buf io.ByteBuffer;
+ tw := makeTabwriter(&buf);
+ printer.Fprint(tw, node, mode);
+ tw.Flush();
+ return buf.Data();
+}
+
+
+// Convert x, whatever it is, to text form.
+func toText(x interface{}) []byte {
+ type String interface { String() string }
+
+ switch v := x.(type) {
+ case []byte:
+ return v;
+ case string:
+ return io.StringBytes(v);
+ case String:
+ return io.StringBytes(v.String());
+ case ast.Decl:
+ return nodeText(v, printer.ExportsOnly);
+ case ast.Expr:
+ return nodeText(v, printer.ExportsOnly);
+ }
+ var buf io.ByteBuffer;
+ fmt.Fprint(&buf, x);
+ return buf.Data();
+}
+
+
+// Template formatter for "html" format.
+func htmlFmt(w io.Writer, x interface{}, format string) {
+ template.HtmlEscape(w, toText(x));
+}
+
+
+// Template formatter for "html-comment" format.
+func htmlCommentFmt(w io.Writer, x interface{}, format string) {
+ doc.ToHtml(w, toText(x));
+}
+
+
+// Template formatter for "" (default) format.
+func textFmt(w io.Writer, x interface{}, format string) {
+ w.Write(toText(x));
+}
+
+
+var fmap = template.FormatterMap{
+ "": textFmt,
+ "html": htmlFmt,
+ "html-comment": htmlCommentFmt,
+}
+
+
+func readTemplate(name string) *template.Template {
+ path := pathutil.Join(*tmplroot, name);
+ data, err := io.ReadFile(path);
+ if err != nil {
+ log.Exitf("ReadFile %s: %v", path, err);
+ }
+ t, err1 := template.Parse(string(data), fmap);
+ if err1 != nil {
+ log.Exitf("%s: %v", name, err);
+ }
+ return t;
+}
+
+
+var godocHtml *template.Template
+var packageHtml *template.Template
+var packageText *template.Template
+var parseerrorHtml *template.Template;
+var parseerrorText *template.Template;
+
+func readTemplates() {
+ // have to delay until after flags processing,
+ // so that main has chdir'ed to goroot.
+ godocHtml = readTemplate("godoc.html");
+ packageHtml = readTemplate("package.html");
+ packageText = readTemplate("package.txt");
+ parseerrorHtml = readTemplate("parseerror.html");
+ parseerrorText = readTemplate("parseerror.txt");
+}
+
+
+// ----------------------------------------------------------------------------
+// Generic HTML wrapper
+
+func servePage(c *http.Conn, title, content interface{}) {
+ type Data struct {
+ title interface{};
+ header interface{};
+ timestamp string;
+ content interface{};
+ }
+
+ var d Data;
+ d.title = title;
+ d.header = title;
+ d.timestamp = time.SecondsToLocalTime(syncTime.get()).String();
+ d.content = content;
+ godocHtml.Execute(&d, c);
+}
+
+
+func serveText(c *http.Conn, text []byte) {
+ c.SetHeader("content-type", "text/plain; charset=utf-8");
+ c.Write(text);
+}
+
+
+// ----------------------------------------------------------------------------
+// Files
+
+func serveParseErrors(c *http.Conn, errors *parseErrors) {
+ // format errors
+ var buf io.ByteBuffer;
+ parseerrorHtml.Execute(errors, &buf);
+ servePage(c, errors.filename + " - Parse Errors", buf.Data());
+}
+
+
+func serveGoSource(c *http.Conn, name string) {
+ prog, errors := parse(name, parser.ParseComments);
+ if errors != nil {
+ serveParseErrors(c, errors);
+ return;
+ }
+
+ var buf io.ByteBuffer;
+ fmt.Fprintln(&buf, ""); + template.HtmlEscape(&buf, nodeText(prog, printer.DocComments)); + fmt.Fprintln(&buf, ""); + + servePage(c, name + " - Go source", buf.Data()); +} + + +var fileServer = http.FileServer(".", ""); + +func serveFile(c *http.Conn, req *http.Request) { + // pick off special cases and hand the rest to the standard file server + switch { + case req.Url.Path == "/": + // serve landing page. + // TODO: hide page from ordinary file serving. + // writing doc/index.html will take care of that. + http.ServeFile(c, req, "doc/root.html"); + + case req.Url.Path == "/doc/root.html": + // hide landing page from its real name + // TODO why - there is no reason for this (remove eventually) + http.NotFound(c, req); + + case pathutil.Ext(req.Url.Path) == ".go": + serveGoSource(c, req.Url.Path[1 : len(req.Url.Path)]); // strip leading '/' from name + + default: + // TODO not good enough - don't want to download files + // want to see them + fileServer.ServeHTTP(c, req); + } +} + + +// ---------------------------------------------------------------------------- +// Packages + +type pakDesc struct { + dirname string; // relative to goroot + pakname string; // same as last component of importpath + importpath string; // import "___" + filenames map[string] bool; // set of file (names) belonging to this package +} + + +// TODO if we don't plan to use the directory information, simplify to []string +type dirList []*os.Dir + +func (d dirList) Len() int { return len(d) } +func (d dirList) Less(i, j int) bool { return d[i].Name < d[j].Name } +func (d dirList) Swap(i, j int) { d[i], d[j] = d[j], d[i] } + + +func isPackageFile(dirname, filename, pakname string) bool { + // ignore test files + if strings.HasSuffix(filename, "_test.go") { + return false; + } + + // determine package name + prog, errors := parse(dirname + "/" + filename, parser.PackageClauseOnly); + if prog == nil { + return false; + } + + return prog != nil && prog.Name.Value == pakname; +} + + +// Returns the canonical URL path, the package denoted by path, and +// the list of sub-directories in the corresponding package directory. +// If there is no such package, the package descriptor pd is nil. +// If there are no sub-directories, the dirs list is nil. +func findPackage(path string) (canonical string, pd *pakDesc, dirs dirList) { + canonical = pathutil.Clean(Pkg + path) + "/"; + + // get directory contents, if possible + importpath := pathutil.Clean(path); // no trailing '/' + dirname := pathutil.Join(*pkgroot, importpath); + if !isDir(dirname) { + return; + } + + fd, err1 := os.Open(dirname, os.O_RDONLY, 0); + if err1 != nil { + log.Stderrf("open %s: %v", dirname, err1); + return; + } + + list, err2 := fd.Readdir(-1); + if err2 != nil { + log.Stderrf("readdir %s: %v", dirname, err2); + return; + } + + // the package name is is the directory name within its parent + _, pakname := pathutil.Split(dirname); + + // collect all files belonging to the package and count the + // number of sub-directories + filenames := make(map[string]bool); + nsub := 0; + for i, entry := range list { + switch { + case isGoFile(&entry) && isPackageFile(dirname, entry.Name, pakname): + // add file to package desc + if tmp, found := filenames[entry.Name]; found { + panic("internal error: same file added more than once: " + entry.Name); + } + filenames[entry.Name] = true; + case isPkgDir(&entry): + nsub++; + } + } + + // make the list of sub-directories, if any + var subdirs dirList; + if nsub > 0 { + subdirs = make(dirList, nsub); + nsub = 0; + for i, entry := range list { + if isPkgDir(&entry) { + // make a copy here so sorting (and other code) doesn't + // have to make one every time an entry is moved + copy := new(os.Dir); + *copy = entry; + subdirs[nsub] = copy; + nsub++; + } + } + sort.Sort(subdirs); + } + + // if there are no package files, then there is no package + if len(filenames) == 0 { + return canonical, nil, subdirs; + } + + return canonical, &pakDesc{dirname, pakname, importpath, filenames}, subdirs; +} + + +func (p *pakDesc) Doc() (*doc.PackageDoc, *parseErrors) { + if p == nil { + return nil, nil; + } + + // compute documentation + var r doc.DocReader; + i := 0; + for filename := range p.filenames { + prog, err := parse(p.dirname + "/" + filename, parser.ParseComments); + if err != nil { + return nil, err; + } + if i == 0 { + // first file - initialize doc + r.Init(prog.Name.Value, p.importpath); + } + i++; + r.AddProgram(prog); + } + + return r.Doc(), nil; +} + + +type PageInfo struct { + PDoc *doc.PackageDoc; + Dirs dirList; +} + +func servePkg(c *http.Conn, r *http.Request) { + path := r.Url.Path; + path = path[len(Pkg) : len(path)]; + canonical, desc, dirs := findPackage(path); + + if r.Url.Path != canonical { + http.Redirect(c, canonical, http.StatusMovedPermanently); + return; + } + + pdoc, errors := desc.Doc(); + if errors != nil { + serveParseErrors(c, errors); + return; + } + + var buf io.ByteBuffer; + if false { // TODO req.Params["format"] == "text" + err := packageText.Execute(PageInfo{pdoc, dirs}, &buf); + if err != nil { + log.Stderrf("packageText.Execute: %s", err); + } + serveText(c, buf.Data()); + return; + } + + err := packageHtml.Execute(PageInfo{pdoc, dirs}, &buf); + if err != nil { + log.Stderrf("packageHtml.Execute: %s", err); + } + + if path == "" { + path = "."; // don't display an empty path + } + servePage(c, path + " - Go package documentation", buf.Data()); +} + + +// ---------------------------------------------------------------------------- +// Server + +func loggingHandler(h http.Handler) http.Handler { + return http.HandlerFunc(func(c *http.Conn, req *http.Request) { + log.Stderrf("%s\t%s", c.RemoteAddr, req.Url); + h.ServeHTTP(c, req); + }) +} + + +func exec(c *http.Conn, args []string) bool { + r, w, err := os.Pipe(); + if err != nil { + log.Stderrf("os.Pipe(): %v\n", err); + return false; + } + + bin := args[0]; + fds := []*os.File{nil, w, w}; + if *verbose { + log.Stderrf("executing %v", args); + } + pid, err := os.ForkExec(bin, args, os.Environ(), goroot, fds); + defer r.Close(); + w.Close(); + if err != nil { + log.Stderrf("os.ForkExec(%q): %v\n", bin, err); + return false; + } + + var buf io.ByteBuffer; + io.Copy(r, &buf); + wait, err := os.Wait(pid, 0); + if err != nil { + os.Stderr.Write(buf.Data()); + log.Stderrf("os.Wait(%d, 0): %v\n", pid, err); + return false; + } + if !wait.Exited() || wait.ExitStatus() != 0 { + os.Stderr.Write(buf.Data()); + log.Stderrf("executing %v failed (exit status = %d)", args, wait.ExitStatus()); + return false; + } + + if *verbose { + os.Stderr.Write(buf.Data()); + } + if c != nil { + c.SetHeader("content-type", "text/plain; charset=utf-8"); + c.Write(buf.Data()); + } + + return true; +} + + +func sync(c *http.Conn, r *http.Request) { + args := []string{"/bin/sh", "-c", *syncCmd}; + if !exec(c, args) { + *syncMin = 0; // disable sync + return; + } + syncTime.set(); +} + + +func usage() { + fmt.Fprintf(os.Stderr, + "usage: godoc package [name ...]\n" + " godoc -http=:6060\n" + ); + flag.PrintDefaults(); + os.Exit(1); +} + + +func main() { + flag.Parse(); + + // Check usage first; get usage message out early. + switch { + case *httpaddr != "": + if flag.NArg() != 0 { + usage(); + } + default: + if flag.NArg() == 0 { + usage(); + } + } + + if err := os.Chdir(goroot); err != nil { + log.Exitf("chdir %s: %v", goroot, err); + } + + readTemplates(); + + if *httpaddr != "" { + var handler http.Handler = http.DefaultServeMux; + if *verbose { + log.Stderrf("Go Documentation Server\n"); + log.Stderrf("address = %s\n", *httpaddr); + log.Stderrf("goroot = %s\n", goroot); + log.Stderrf("pkgroot = %s\n", *pkgroot); + log.Stderrf("tmplroot = %s\n", *tmplroot); + handler = loggingHandler(handler); + } + + http.Handle(Pkg, http.HandlerFunc(servePkg)); + if *syncCmd != "" { + http.Handle("/debug/sync", http.HandlerFunc(sync)); + } + http.Handle("/", http.HandlerFunc(serveFile)); + + // The server may have been restarted; always wait 1sec to + // give the forking server a chance to shut down and release + // the http port. + time.Sleep(1e9); + + // Start sync goroutine, if enabled. + if *syncCmd != "" && *syncMin > 0 { + go func() { + if *verbose { + log.Stderrf("sync every %dmin", *syncMin); + } + for *syncMin > 0 { + sync(nil, nil); + time.Sleep(int64(*syncMin) * (60 * 1e9)); + } + if *verbose { + log.Stderrf("periodic sync stopped"); + } + }(); + } + + if err := http.ListenAndServe(*httpaddr, handler); err != nil { + log.Exitf("ListenAndServe %s: %v", *httpaddr, err) + } + return; + } + + if *html { + packageText = packageHtml; + parseerrorText = parseerrorHtml; + } + + _, desc, dirs := findPackage(flag.Arg(0)); + pdoc, errors := desc.Doc(); + if errors != nil { + err := parseerrorText.Execute(errors, os.Stderr); + if err != nil { + log.Stderrf("parseerrorText.Execute: %s", err); + } + os.Exit(1); + } + + if pdoc != nil && flag.NArg() > 1 { + args := flag.Args(); + pdoc.Filter(args[1 : len(args)]); + } + + packageText.Execute(PageInfo{pdoc, dirs}, os.Stdout); +} diff --git a/src/cmd/make.bash b/src/cmd/make.bash index ca4517218e..2019b440ff 100644 --- a/src/cmd/make.bash +++ b/src/cmd/make.bash @@ -18,7 +18,7 @@ bash mkenam make enam.o cd .. -for i in cc ${O}l ${O}a ${O}c gc ${O}g ar db nm acid cov godefs prof gotest +for i in cc ${O}l ${O}a ${O}c gc ${O}g ar db nm acid cov godefs godoc prof gotest do echo; echo; echo %%%% making $i %%%%; echo cd $i