// godoc: Go Documentation Server
+// Web server tree:
+//
+// http://godoc/ main landing page (TODO)
+// http://godoc/doc/ serve from $GOROOT/doc - spec, mem, tutorial, etc. (TODO)
+// 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 proto
+// godoc compress/zlib Cipher NewCMAC
+// - prints doc for Cipher and NewCMAC in package crypto/block
+
+
package main
import (
"net";
"os";
"parser";
+ pathutil "path";
"sort";
"tabwriter";
"template";
"time";
"token";
- "regexp";
"vector";
"astprinter";
- "compilation"; // TODO removing this causes link errors - why?
"docprinter";
)
// TODO
// - uniform use of path, filename, dirname, pakname, etc.
// - fix weirdness with double-/'s in paths
-// - cleanup uses of *root, GOROOT, etc. (quite a mess at the moment)
+// - split http service into its own source file
+
+
+const usageString =
+ "usage: godoc package [name ...]\n"
+ " godoc -http=:6060\n"
const (
)
-func getenv(varname string) string {
- value, err := os.Getenv(varname);
- return value;
-}
-
-
var (
- GOROOT = getenv("GOROOT");
+ goroot string;
- // server control
verbose = flag.Bool("v", false, "verbose mode");
- port = flag.String("port", "6060", "server port");
- root = flag.String("root", GOROOT, "root directory");
+
+ // server control
+ httpaddr = flag.String("http", "", "HTTP service address (e.g., ':6060')");
// layout control
tabwidth = flag.Int("tabwidth", 4, "tab width");
- usetabs = flag.Bool("usetabs", false, "align with tabs instead of blanks");
+ usetabs = flag.Bool("tabs", false, "align with tabs instead of spaces");
)
-// ----------------------------------------------------------------------------
-// Support
-
-func cleanPath(s string) string {
- for i := 0; i < len(s); i++ {
- if s[i] == '/' {
- i++;
- j := i;
- for j < len(s) && s[j] == '/' {
- j++;
- }
- if j > i { // more then one '/'
- return s[0 : i] + cleanPath(s[j : len(s)]);
- }
- }
+func init() {
+ var err *os.Error;
+ goroot, err = os.Getenv("GOROOT");
+ if err != nil {
+ goroot = "/home/r/go-build/go";
}
- return s;
+ flag.StringVar(&goroot, "goroot", goroot, "Go root directory");
}
-// Reduce sequences of multiple '/'s into a single '/' and
-// strip any trailing '/' (may result in the empty string).
-func sanitizePath(s string) string {
- s = cleanPath(s);
- if len(s) > 0 && s[len(s)-1] == '/' { // strip trailing '/'
- s = s[0 : len(s)-1];
- }
- return s;
-}
-
+// ----------------------------------------------------------------------------
+// Support
func hasPrefix(s, prefix string) bool {
return len(prefix) <= len(s) && s[0 : len(prefix)] == prefix;
}
-func hasSuffix(s, postfix string) bool {
- pos := len(s) - len(postfix);
- return pos >= 0 && s[pos : len(s)] == postfix;
+func hasSuffix(s, suffix string) bool {
+ pos := len(s) - len(suffix);
+ return pos >= 0 && s[pos : len(s)] == suffix;
}
}
-func printLink(c *http.Conn, path, name string) {
- fmt.Fprintf(c, "<a href=\"%s\">%s</a><br />\n", filePrefix + path + name, name);
+func isDir(name string) bool {
+ d, err := os.Stat(name);
+ return err == nil && d.IsDirectory();
+}
+
+
+func isFile(name string) bool {
+ d, err := os.Stat(name);
+ return err == nil && d.IsRegular();
+}
+
+
+func printLink(c *http.Conn, dir, name string) {
+ fmt.Fprintf(c, "<a href=\"%s\">%s</a><br />\n", pathutil.Clean(filePrefix + dir + "/" + name), name);
}
// ----------------------------------------------------------------------------
-// Compilation
+// Parsing
type parseError struct {
pos token.Position;
func (h *errorHandler) Error(pos token.Position, msg string) {
- // only collect errors that are on a new line
+ // only collect errors that are on a new line
// in the hope to avoid most follow-up errors
if pos.Line != h.lastLine {
h.lastLine = pos.Line;
}
-// Compiles a file (path) and returns the corresponding AST and
+// Parses a file (path) and returns the corresponding AST and
// a sorted list (by file position) of errors, if any.
//
-func compile(path string, mode uint) (*ast.Program, errorList) {
+func parse(path string, mode uint) (*ast.Program, errorList) {
src, err := os.Open(path, os.O_RDONLY, 0);
defer src.Close();
if err != nil {
- log.Stdoutf("%s: %v", path, err);
+ log.Stdoutf("open %s: %v", path, err);
var noPos token.Position;
return nil, errorList{parseError{noPos, err.String()}};
}
func servePage(c *http.Conn, title string, contents func()) {
c.SetHeader("content-type", "text/html; charset=utf-8");
-
+
// TODO handle Apply errors
godoc_html.Apply(c, "<!--", template.Substitution {
"TITLE-->" : func() { fmt.Fprint(c, title); },
func serveDir(c *http.Conn, dirname string) {
- fd, err1 := os.Open(*root + dirname, os.O_RDONLY, 0);
+ fd, err1 := os.Open(dirname, os.O_RDONLY, 0);
if err1 != nil {
c.WriteHeader(http.StatusNotFound);
fmt.Fprintf(c, "Error: %v (%s)\n", err1, dirname);
// ----------------------------------------------------------------------------
// Files
-func serveCompilationErrors(c *http.Conn, filename string, errors errorList) {
+func serveParseErrors(c *http.Conn, filename string, errors errorList) {
// open file
- path := *root + filename;
+ path := filename;
fd, err1 := os.Open(path, os.O_RDONLY, 0);
defer fd.Close();
if err1 != nil {
// TODO handle Apply errors
servePage(c, filename, func () {
// section title
- fmt.Fprintf(c, "<h1>Compilation errors in %s</h1>\n", filename);
-
+ fmt.Fprintf(c, "<h1>Parse errors in %s</h1>\n", filename);
+
// handle read errors
- if err1 != nil || err2 != nil /* 6g bug139 */ {
+ if err1 != nil || err2 != nil {
fmt.Fprintf(c, "could not read file %s\n", filename);
return;
}
-
+
// write source with error messages interspersed
fmt.Fprintln(c, "<pre>");
offs := 0;
func serveGoSource(c *http.Conn, dirname string, filename string) {
path := dirname + "/" + filename;
- prog, errors := compile(*root + "/" + path, parser.ParseComments);
+ prog, errors := parse(path, parser.ParseComments);
if len(errors) > 0 {
- serveCompilationErrors(c, filename, errors);
+ serveParseErrors(c, filename, errors);
return;
}
serveError(c, err1.String(), filename);
return
}
- written, err2 := io.Copy(src, c);
- if err2 != nil {
+ if written, err2 := io.Copy(src, c); err2 != nil {
serveError(c, err2.String(), filename);
return
}
func serveFile(c *http.Conn, path string) {
- dir, err := os.Stat(*root + path);
+ dir, err := os.Stat(path);
if err != nil {
serveError(c, err.String(), path);
return;
case dir.IsDirectory():
serveDir(c, path);
case isGoFile(dir):
- serveGoSource(c, "", path);
+ serveGoSource(c, ".", path);
case isHTMLFile(dir):
- serveHTMLFile(c, *root + path);
+ serveHTMLFile(c, path);
default:
serveError(c, "Not a directory or .go file", path);
}
// Packages
type pakDesc struct {
- dirname string; // local to *root
- pakname string; // local to directory
+ dirname string; // relative to goroot
+ pakname string; // relative to directory
filenames map[string] bool; // set of file (names) belonging to this package
}
func (p pakArray) Swap(i, j int) { p[i], p[j] = p[j], p[i]; }
-// The global list of packages (sorted)
-// TODO should be accessed under a lock
-var pakList pakArray;
-
-
func addFile(pmap map[string]*pakDesc, dirname string, filename string) {
if hasSuffix(filename, "_test.go") {
// ignore package tests
return;
}
// determine package name
- path := *root + "/" + dirname + "/" + filename;
- prog, errors := compile(path, parser.PackageClauseOnly);
+ path := dirname + "/" + filename;
+ prog, errors := parse(path, parser.PackageClauseOnly);
if prog == nil {
return;
}
// ignore main packages for now
return;
}
- pakname := dirname + "/" + prog.Name.Value;
+ pakname := pathutil.Clean(dirname + "/" + prog.Name.Value);
// find package descriptor
pakdesc, found := pmap[pakname];
pakdesc = &pakDesc{dirname, prog.Name.Value, make(map[string]bool)};
pmap[pakname] = pakdesc;
}
-
+
//fmt.Printf("pak = %s, file = %s\n", pakname, filename);
// add file to package desc
func addDirectory(pmap map[string]*pakDesc, dirname string) {
- // TODO should properly check device and inode to see if we have
- // traversed this directory already
- fd, err1 := os.Open(*root + dirname, os.O_RDONLY, 0);
+ path := dirname;
+ fd, err1 := os.Open(path, os.O_RDONLY, 0);
if err1 != nil {
- log.Stdoutf("%s: %v", *root + dirname, err1);
+ log.Stdoutf("open %s: %v", path, err1);
return;
}
list, err2 := fd.Readdir(-1);
if err2 != nil {
- log.Stdoutf("%s: %v", *root + dirname, err2);
+ log.Stdoutf("readdir %s: %v", path, err2);
return;
}
for i, entry := range list {
switch {
- case entry.IsDirectory():
- if entry.Name != "." && entry.Name != ".." {
- addDirectory(pmap, dirname + "/" + entry.Name);
- }
- case isGoFile(&entry):
+ case isGoFile(&entry):
//fmt.Printf("found %s/%s\n", dirname, entry.Name);
addFile(pmap, dirname, entry.Name);
}
}
-func makePackageMap() {
- // TODO shold do this under a lock
- // populate package map
- pmap := make(map[string]*pakDesc);
- addDirectory(pmap, "");
-
+func mapValues(pmap map[string]*pakDesc) pakArray {
// build sorted package list
plist := make(pakArray, len(pmap));
i := 0;
i++;
}
sort.Sort(plist);
-
- // install package list (TODO should do this under a lock)
- pakList = plist;
-
- if *verbose {
- log.Stdoutf("%d packages found under %s", i, *root);
- }
+ return plist;
}
filenames[i] = filename;
i++;
}
-
+
// compute documentation
var doc docPrinter.PackageDoc;
for i, filename := range filenames {
- path := *root + "/" + p.dirname + "/" + filename;
- prog, errors := compile(path, parser.ParseComments);
+ path := p.dirname + "/" + filename;
+ prog, errors := parse(path, parser.ParseComments);
if len(errors) > 0 {
- serveCompilationErrors(c, filename, errors);
+ serveParseErrors(c, filename, errors);
return;
}
}
-func servePackageList(c *http.Conn, list *vector.Vector) {
+func servePackageList(c *http.Conn, list pakArray) {
servePage(c, "Packages", func () {
- for i := 0; i < list.Len(); i++ {
- p := list.At(i).(*pakDesc);
- link := p.dirname + "/" + p.pakname;
- fmt.Fprintf(c, "<a href=\"%s\">%s</a> <font color=grey>(%s)</font><br />\n", docPrefix + link, p.pakname, link);
+ for i := 0; i < len(list); i++ {
+ p := list[i];
+ link := pathutil.Clean(p.dirname + "/" + p.pakname);
+ fmt.Fprintf(c, "<a href=\"%s\">%s</a> <font color=grey>(%s)</font><br />\n",
+ p.pakname, p.pakname, link);
}
});
+
+ // TODO: show subdirectories
}
-func serveDoc(c *http.Conn, path string) {
- // make regexp for package matching
- rex, err := regexp.Compile(path);
- if err != nil {
- serveError(c, err.String(), path);
- return;
+// Return package or packages named by name.
+// Name is either an import string or a directory,
+// like you'd see in $GOROOT/pkg/ once the 6g
+// tools can handle a hierarchy there.
+//
+// Examples:
+// "math" - single package made up of directory
+// "container" - directory listing
+// "container/vector" - single package in container directory
+func findPackages(name string) (*pakDesc, pakArray) {
+ // Build list of packages.
+ // If the path names a directory, scan that directory
+ // for a package with the name matching the directory name.
+ // Otherwise assume it is a package name inside
+ // a directory, so scan the parent.
+ pmap := make(map[string]*pakDesc);
+ dir := pathutil.Clean("src/lib/" + name);
+ if isDir(dir) {
+ parent, pak := pathutil.Split(dir);
+ addDirectory(pmap, dir);
+ paks := mapValues(pmap);
+ if len(paks) == 1 {
+ p := paks[0];
+ if p.dirname == dir && p.pakname == pak {
+ return p, nil;
+ }
+ }
+ return nil, paks;
}
- // build list of matching packages
- list := vector.New(0);
- for i, p := range pakList {
- if rex.Match(p.dirname + "/" + p.pakname) {
- list.Push(p);
- }
+ // Otherwise, have parentdir/pak. Look for package pak in dir.
+ parentdir, pak := pathutil.Split(dir);
+ addDirectory(pmap, parentdir);
+ if p, ok := pmap[dir]; ok {
+ return p, nil;
}
- switch list.Len() {
- case 0:
- serveError(c, "No packages found", path);
- case 1:
- servePackage(c, list.At(0).(*pakDesc));
+ return nil, nil;
+}
+
+
+func servePkg(c *http.Conn, path string) {
+ pak, paks := findPackages(path);
+
+ // TODO: canonicalize path and redirect if needed.
+
+ switch {
+ case pak != nil:
+ servePackage(c, pak);
+ case len(paks) > 0:
+ servePackageList(c, paks);
default:
- servePackageList(c, list);
+ serveError(c, "No packages found", path);
}
}
func makeFixedFileServer(filename string) (func(c *http.Conn, path string)) {
return func(c *http.Conn, path string) {
- // ignore path and always serve the same file
- // TODO this should be serveFile but there are some issues with *root
- serveHTMLFile(c, filename);
+ serveFile(c, filename);
};
}
if *verbose {
log.Stdoutf("%s\t%s", req.Host, path);
}
- if hasPrefix(path, prefix) {
- path = sanitizePath(path[len(prefix) : len(path)]);
- //log.Stdoutf("sanitized path %s", path);
- handler(c, path);
- } else {
- log.Stdoutf("illegal path %s", path);
- }
+ handler(c, path[len(prefix) : len(path)]);
};
// install the customized handler
}
+func usage() {
+ fmt.Fprintf(os.Stderr, usageString);
+ sys.Exit(1);
+}
+
+
func main() {
flag.Parse();
- *root = sanitizePath(*root);
- { dir, err := os.Stat(*root);
- if err != nil || !dir.IsDirectory() {
- log.Exitf("root not found or not a directory: %s", *root);
+ // Check usage first; get usage message out early.
+ switch {
+ case *httpaddr != "":
+ if flag.NArg() != 0 {
+ usage();
+ }
+ default:
+ if flag.NArg() == 0 {
+ usage();
}
}
- if *verbose {
- log.Stdoutf("Go Documentation Server\n");
- log.Stdoutf("port = %s\n", *port);
- log.Stdoutf("root = %s\n", *root);
+ if err := os.Chdir(goroot); err != nil {
+ log.Exitf("chdir %s: %v", goroot, err);
}
- makePackageMap();
-
- installHandler("/mem", makeFixedFileServer(GOROOT + "/doc/go_mem.html"));
- installHandler("/spec", makeFixedFileServer(GOROOT + "/doc/go_spec.html"));
- installHandler(docPrefix, serveDoc);
- installHandler(filePrefix, serveFile);
+ if *httpaddr != "" {
+ if *verbose {
+ log.Stdoutf("Go Documentation Server\n");
+ log.Stdoutf("address = %s\n", *httpaddr);
+ log.Stdoutf("goroot = %s\n", goroot);
+ }
+
+ installHandler("/mem", makeFixedFileServer("doc/go_mem.html"));
+ installHandler("/spec", makeFixedFileServer("doc/go_spec.html"));
+ installHandler("/pkg/", servePkg);
+ installHandler(filePrefix, serveFile);
- { err := http.ListenAndServe(":" + *port, nil);
- if err != nil {
- log.Exitf("ListenAndServe: %v", err)
+ if err := http.ListenAndServe(*httpaddr, nil); err != nil {
+ log.Exitf("ListenAndServe %s: %v", *httpaddr, err)
}
+ return;
}
+
+ log.Exitf("godoc command-line not implemented");
}