From b844907f9f42f55bff7017f48247e9d97708d61b Mon Sep 17 00:00:00 2001 From: Brad Garcia Date: Wed, 20 Nov 2013 09:56:10 -0500 Subject: [PATCH] godoc: refactor command-line mode handling. R=bradfitz CC=golang-dev https://golang.org/cl/29230044 --- cmd/godoc/handlers.go | 2 +- cmd/godoc/main.go | 179 +----------------------------------- godoc/cmdline.go | 209 ++++++++++++++++++++++++++++++++++++++++++ godoc/cmdline_test.go | 168 +++++++++++++++++++++++++++++++++ godoc/pres.go | 5 + 5 files changed, 388 insertions(+), 175 deletions(-) create mode 100644 godoc/cmdline.go create mode 100644 godoc/cmdline_test.go diff --git a/cmd/godoc/handlers.go b/cmd/godoc/handlers.go index 9b1b56add7..99cd215204 100644 --- a/cmd/godoc/handlers.go +++ b/cmd/godoc/handlers.go @@ -62,7 +62,7 @@ func readTemplates(p *godoc.Presentation, html bool) { p.PackageText = readTemplate("package.txt") p.SearchText = readTemplate("search.txt") - if html { + if html || p.HTMLMode { codewalkHTML = readTemplate("codewalk.html") codewalkdirHTML = readTemplate("codewalkdir.html") p.DirlistHTML = readTemplate("dirlist.html") diff --git a/cmd/godoc/main.go b/cmd/godoc/main.go index 8c9a7b6eb0..544347a7ad 100644 --- a/cmd/godoc/main.go +++ b/cmd/godoc/main.go @@ -29,24 +29,19 @@ package main import ( "archive/zip" - "bytes" _ "expvar" // to serve /debug/vars "flag" "fmt" - "go/ast" "go/build" - "go/printer" "log" "net/http" "net/http/httptest" _ "net/http/pprof" // to serve /debug/pprof/* "net/url" "os" - pathpkg "path" "path/filepath" "regexp" "runtime" - "strings" "code.google.com/p/go.tools/godoc" "code.google.com/p/go.tools/godoc/static" @@ -120,32 +115,6 @@ func loggingHandler(h http.Handler) http.Handler { }) } -// Does s look like a regular expression? -func isRegexp(s string) bool { - return strings.IndexAny(s, ".(|)*+?^$[]") >= 0 -} - -// Make a regular expression of the form -// names[0]|names[1]|...names[len(names)-1]. -// Returns nil if the regular expression is illegal. -func makeRx(names []string) (rx *regexp.Regexp) { - if len(names) > 0 { - s := "" - for i, name := range names { - if i > 0 { - s += "|" - } - if isRegexp(name) { - s += name - } else { - s += "^" + name + "$" // must match exactly - } - } - rx, _ = regexp.Compile(s) // rx is nil if there's a compilation error - } - return -} - func handleURLFlag() { // Try up to 10 fetches, following redirects. urlstr := *urlFlag @@ -239,11 +208,13 @@ func main() { pres.ShowPlayground = *showPlayground pres.ShowExamples = *showExamples pres.DeclLinks = *declLinks + pres.SrcMode = *srcMode + pres.HTMLMode = *html if *notesRx != "" { pres.NotesRx = regexp.MustCompile(*notesRx) } - readTemplates(pres, httpMode || *urlFlag != "" || *html) + readTemplates(pres, httpMode || *urlFlag != "") registerHandlers(pres) if *writeIndex { @@ -312,152 +283,12 @@ func main() { return } - packageText := pres.PackageText - - // Command line mode. - if *html { - packageText = pres.PackageHTML - } - if *query { handleRemoteSearch() return } - // Determine paths. - // - // If we are passed an operating system path like . or ./foo or /foo/bar or c:\mysrc, - // we need to map that path somewhere in the fs name space so that routines - // like getPageInfo will see it. We use the arbitrarily-chosen virtual path "/target" - // for this. That is, if we get passed a directory like the above, we map that - // directory so that getPageInfo sees it as /target. - const target = "/target" - const cmdPrefix = "cmd/" - path := flag.Arg(0) - var forceCmd bool - var abspath, relpath string - if filepath.IsAbs(path) { - fs.Bind(target, vfs.OS(path), "/", vfs.BindReplace) - abspath = target - } else if build.IsLocalImport(path) { - cwd, _ := os.Getwd() // ignore errors - path = filepath.Join(cwd, path) - fs.Bind(target, vfs.OS(path), "/", vfs.BindReplace) - abspath = target - } else if strings.HasPrefix(path, cmdPrefix) { - path = strings.TrimPrefix(path, cmdPrefix) - forceCmd = true - } else if bp, _ := build.Import(path, "", build.FindOnly); bp.Dir != "" && bp.ImportPath != "" { - fs.Bind(target, vfs.OS(bp.Dir), "/", vfs.BindReplace) - abspath = target - relpath = bp.ImportPath - } else { - abspath = pathpkg.Join(pres.PkgFSRoot(), path) - } - if relpath == "" { - relpath = abspath - } - - var mode godoc.PageInfoMode - if relpath == "builtin" { - // the fake built-in package contains unexported identifiers - mode = godoc.NoFiltering | godoc.NoFactoryFuncs - } - if *srcMode { - // only filter exports if we don't have explicit command-line filter arguments - if flag.NArg() > 1 { - mode |= godoc.NoFiltering - } - mode |= godoc.ShowSource - } - - // First, try as package unless forced as command. - var info *godoc.PageInfo - if !forceCmd { - info = pres.GetPkgPageInfo(abspath, relpath, mode) - } - - // Second, try as command (if the path is not absolute). - var cinfo *godoc.PageInfo - if !filepath.IsAbs(path) { - // First try go.tools/cmd. - abspath = pathpkg.Join(pres.PkgFSRoot(), toolsPath+path) - cinfo = pres.GetCmdPageInfo(abspath, relpath, mode) - if cinfo.IsEmpty() { - // Then try $GOROOT/cmd. - abspath = pathpkg.Join(pres.CmdFSRoot(), path) - cinfo = pres.GetCmdPageInfo(abspath, relpath, mode) - } - } - - // determine what to use - if info == nil || info.IsEmpty() { - if cinfo != nil && !cinfo.IsEmpty() { - // only cinfo exists - switch to cinfo - info = cinfo - } - } else if cinfo != nil && !cinfo.IsEmpty() { - // both info and cinfo exist - use cinfo if info - // contains only subdirectory information - if info.PAst == nil && info.PDoc == nil { - info = cinfo - } else { - fmt.Printf("use 'godoc %s%s' for documentation on the %s command \n\n", cmdPrefix, relpath, relpath) - } - } - - if info == nil { - log.Fatalf("%s: no such directory or package", flag.Arg(0)) - } - if info.Err != nil { - log.Fatalf("%v", info.Err) - } - - if info.PDoc != nil && info.PDoc.ImportPath == target { - // Replace virtual /target with actual argument from command line. - info.PDoc.ImportPath = flag.Arg(0) - } - - // If we have more than one argument, use the remaining arguments for filtering. - if flag.NArg() > 1 { - args := flag.Args()[1:] - rx := makeRx(args) - if rx == nil { - log.Fatalf("illegal regular expression from %v", args) - } - - filter := func(s string) bool { return rx.MatchString(s) } - switch { - case info.PAst != nil: - cmap := ast.NewCommentMap(info.FSet, info.PAst, info.PAst.Comments) - ast.FilterFile(info.PAst, filter) - // Special case: Don't use templates for printing - // so we only get the filtered declarations without - // package clause or extra whitespace. - for i, d := range info.PAst.Decls { - // determine the comments associated with d only - comments := cmap.Filter(d).Comments() - cn := &printer.CommentedNode{Node: d, Comments: comments} - if i > 0 { - fmt.Println() - } - if *html { - var buf bytes.Buffer - pres.WriteNode(&buf, info.FSet, cn) - godoc.FormatText(os.Stdout, buf.Bytes(), -1, true, "", nil) - } else { - pres.WriteNode(os.Stdout, info.FSet, cn) - } - fmt.Println() - } - return - - case info.PDoc != nil: - info.PDoc.Filter(filter) - } - } - - if err := packageText.Execute(os.Stdout, info); err != nil { - log.Printf("packageText.Execute: %s", err) + if err := godoc.CommandLine(os.Stdout, fs, pres, flag.Args()); err != nil { + log.Print(err) } } diff --git a/godoc/cmdline.go b/godoc/cmdline.go new file mode 100644 index 0000000000..7b7d7ba97b --- /dev/null +++ b/godoc/cmdline.go @@ -0,0 +1,209 @@ +// Copyright 2013 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. +package godoc + +import ( + "bytes" + "fmt" + "go/ast" + "go/build" + "go/printer" + "io" + "log" + "os" + pathpkg "path" + "path/filepath" + "regexp" + "strings" + + "code.google.com/p/go.tools/godoc/vfs" +) + +const ( + target = "/target" + cmdPrefix = "cmd/" + srcPrefix = "src/" + toolsPath = "code.google.com/p/go.tools/cmd/" +) + +// CommandLine returns godoc results to w. +// Note that it may add a /target path to fs. +func CommandLine(w io.Writer, fs vfs.NameSpace, pres *Presentation, args []string) error { + path := args[0] + srcMode := pres.SrcMode + cmdMode := strings.HasPrefix(path, cmdPrefix) + if strings.HasPrefix(path, srcPrefix) { + path = strings.TrimPrefix(path, srcPrefix) + srcMode = true + } + var abspath, relpath string + if cmdMode { + path = strings.TrimPrefix(path, cmdPrefix) + } else { + abspath, relpath = paths(fs, pres, path) + } + + var mode PageInfoMode + if relpath == "builtin" { + // the fake built-in package contains unexported identifiers + mode = NoFiltering | NoFactoryFuncs + } + if srcMode { + // only filter exports if we don't have explicit command-line filter arguments + if len(args) > 1 { + mode |= NoFiltering + } + mode |= ShowSource + } + + // First, try as package unless forced as command. + var info *PageInfo + if !cmdMode { + info = pres.GetPkgPageInfo(abspath, relpath, mode) + } + + // Second, try as command (if the path is not absolute). + var cinfo *PageInfo + if !filepath.IsAbs(path) { + // First try go.tools/cmd. + abspath = pathpkg.Join(pres.PkgFSRoot(), toolsPath+path) + cinfo = pres.GetCmdPageInfo(abspath, relpath, mode) + if cinfo.IsEmpty() { + // Then try $GOROOT/cmd. + abspath = pathpkg.Join(pres.CmdFSRoot(), path) + cinfo = pres.GetCmdPageInfo(abspath, relpath, mode) + } + } + + // determine what to use + if info == nil || info.IsEmpty() { + if cinfo != nil && !cinfo.IsEmpty() { + // only cinfo exists - switch to cinfo + info = cinfo + } + } else if cinfo != nil && !cinfo.IsEmpty() { + // both info and cinfo exist - use cinfo if info + // contains only subdirectory information + if info.PAst == nil && info.PDoc == nil { + info = cinfo + } else { + fmt.Fprintf(w, "use 'godoc %s%s' for documentation on the %s command \n\n", cmdPrefix, relpath, relpath) + } + } + + if info == nil { + return fmt.Errorf("%s: no such directory or package", args[0]) + } + if info.Err != nil { + return info.Err + } + + if info.PDoc != nil && info.PDoc.ImportPath == target { + // Replace virtual /target with actual argument from command line. + info.PDoc.ImportPath = args[0] + } + + // If we have more than one argument, use the remaining arguments for filtering. + if len(args) > 1 { + filterInfo(pres, args[1:], info) + } + + packageText := pres.PackageText + if pres.HTMLMode { + packageText = pres.PackageHTML + } + if err := packageText.Execute(w, info); err != nil { + return err + } + return nil +} + +// paths determines the paths to use. +// +// If we are passed an operating system path like . or ./foo or /foo/bar or c:\mysrc, +// we need to map that path somewhere in the fs name space so that routines +// like getPageInfo will see it. We use the arbitrarily-chosen virtual path "/target" +// for this. That is, if we get passed a directory like the above, we map that +// directory so that getPageInfo sees it as /target. +// Returns the absolute and relative paths. +func paths(fs vfs.NameSpace, pres *Presentation, path string) (string, string) { + if filepath.IsAbs(path) { + fs.Bind(target, vfs.OS(path), "/", vfs.BindReplace) + return target, target + } + if build.IsLocalImport(path) { + cwd, _ := os.Getwd() // ignore errors + path = filepath.Join(cwd, path) + fs.Bind(target, vfs.OS(path), "/", vfs.BindReplace) + return target, target + } + if bp, _ := build.Import(path, "", build.FindOnly); bp.Dir != "" && bp.ImportPath != "" { + fs.Bind(target, vfs.OS(bp.Dir), "/", vfs.BindReplace) + return target, bp.ImportPath + } + return pathpkg.Join(pres.PkgFSRoot(), path), path +} + +func filterInfo(pres *Presentation, args []string, info *PageInfo) { + rx, err := makeRx(args) + if err != nil { + log.Fatalf("illegal regular expression from %v: %v", args, err) + } + + filter := func(s string) bool { return rx.MatchString(s) } + switch { + case info.PAst != nil: + cmap := ast.NewCommentMap(info.FSet, info.PAst, info.PAst.Comments) + ast.FilterFile(info.PAst, filter) + // Special case: Don't use templates for printing + // so we only get the filtered declarations without + // package clause or extra whitespace. + for i, d := range info.PAst.Decls { + // determine the comments associated with d only + comments := cmap.Filter(d).Comments() + cn := &printer.CommentedNode{Node: d, Comments: comments} + if i > 0 { + fmt.Println() + } + if pres.HTMLMode { + var buf bytes.Buffer + pres.WriteNode(&buf, info.FSet, cn) + FormatText(os.Stdout, buf.Bytes(), -1, true, "", nil) + } else { + pres.WriteNode(os.Stdout, info.FSet, cn) + } + fmt.Println() + } + return + + case info.PDoc != nil: + info.PDoc.Filter(filter) + } +} + +// Does s look like a regular expression? +func isRegexp(s string) bool { + return strings.IndexAny(s, ".(|)*+?^$[]") >= 0 +} + +// Make a regular expression of the form +// names[0]|names[1]|...names[len(names)-1]. +// Returns an error if the regular expression is illegal. +func makeRx(names []string) (*regexp.Regexp, error) { + if len(names) == 0 { + return nil, fmt.Errorf("no expression provided") + } + s := "" + for i, name := range names { + if i > 0 { + s += "|" + } + if isRegexp(name) { + s += name + } else { + s += "^" + name + "$" // must match exactly + } + } + return regexp.Compile(s) +} diff --git a/godoc/cmdline_test.go b/godoc/cmdline_test.go new file mode 100644 index 0000000000..ac05d4d78e --- /dev/null +++ b/godoc/cmdline_test.go @@ -0,0 +1,168 @@ +package godoc + +import ( + "bytes" + "reflect" + "regexp" + "testing" + "text/template" + + "code.google.com/p/go.tools/godoc/vfs" + "code.google.com/p/go.tools/godoc/vfs/mapfs" +) + +func TestPaths(t *testing.T) { + pres := &Presentation{ + pkgHandler: handlerServer{ + fsRoot: "/fsroot", + }, + } + fs := make(vfs.NameSpace) + + for _, tc := range []struct { + desc string + path string + expAbs string + expRel string + }{ + { + "Absolute path", + "/foo/fmt", + "/target", + "/target", + }, + { + "Local import", + "../foo/fmt", + "/target", + "/target", + }, + { + "Import", + "fmt", + "/target", + "fmt", + }, + { + "Default", + "unknownpkg", + "/fsroot/unknownpkg", + "unknownpkg", + }, + } { + abs, rel := paths(fs, pres, tc.path) + if abs != tc.expAbs || rel != tc.expRel { + t.Errorf("%s: paths(%q) = %s,%s; want %s,%s", tc.desc, tc.path, abs, rel, tc.expAbs, tc.expRel) + } + } +} + +func TestMakeRx(t *testing.T) { + for _, tc := range []struct { + desc string + names []string + exp string + }{ + { + desc: "empty string", + names: []string{""}, + exp: `^$`, + }, + { + desc: "simple text", + names: []string{"a"}, + exp: `^a$`, + }, + { + desc: "two words", + names: []string{"foo", "bar"}, + exp: `^foo$|^bar$`, + }, + { + desc: "word & non-trivial", + names: []string{"foo", `ab?c`}, + exp: `^foo$|ab?c`, + }, + { + desc: "bad regexp", + names: []string{`(."`}, + exp: `(."`, + }, + } { + expRE, expErr := regexp.Compile(tc.exp) + if re, err := makeRx(tc.names); !reflect.DeepEqual(err, expErr) && !reflect.DeepEqual(re, expRE) { + t.Errorf("%s: makeRx(%v) = %q,%q; want %q,%q", tc.desc, tc.names, re, err, expRE, expErr) + } + } +} + +func TestCommandLine(t *testing.T) { + mfs := mapfs.New(map[string]string{ + "src/pkg/bar/bar.go": `// Package bar is an example. +package bar +`, + "src/cmd/go/doc.go": `// The go command +package main +`, + "src/cmd/gofmt/doc.go": `// The gofmt command +package main +`, + }) + fs := make(vfs.NameSpace) + fs.Bind("/", mfs, "/", vfs.BindReplace) + c := NewCorpus(fs) + p := &Presentation{Corpus: c} + p.cmdHandler = handlerServer{p, c, "/cmd/", "/src/cmd"} + p.pkgHandler = handlerServer{p, c, "/pkg/", "/src/pkg"} + p.initFuncMap() + p.PackageText = template.Must(template.New("PackageText").Funcs(p.FuncMap()).Parse(`{{with .PAst}}{{node $ .}}{{end}}{{with .PDoc}}{{if $.IsMain}}COMMAND {{.Doc}}{{else}}PACKAGE {{.Doc}}{{end}}{{end}}`)) + + for _, tc := range []struct { + desc string + args []string + exp string + err bool + }{ + { + desc: "standard package", + args: []string{"runtime/race"}, + exp: `PACKAGE Package race implements data race detection logic. +No public interface is provided. +For details about the race detector see +http://golang.org/doc/articles/race_detector.html +`, + }, + { + desc: "package", + args: []string{"bar"}, + exp: "PACKAGE Package bar is an example.\n", + }, + { + desc: "source mode", + args: []string{"src/bar"}, + exp: "// Package bar is an example.\npackage bar\n", + }, + { + desc: "command", + args: []string{"go"}, + exp: "COMMAND The go command\n", + }, + { + desc: "forced command", + args: []string{"cmd/gofmt"}, + exp: "COMMAND The gofmt command\n", + }, + { + desc: "bad arg", + args: []string{"doesnotexist"}, + err: true, + }, + } { + w := new(bytes.Buffer) + err := CommandLine(w, fs, p, tc.args) + if got, want := w.String(), tc.exp; got != want || tc.err == (err == nil) { + t.Errorf("%s: CommandLine(%v) = %q,%v; want %q,%v", + tc.desc, tc.args, got, err, want, tc.err) + } + } +} diff --git a/godoc/pres.go b/godoc/pres.go index 631b62c564..2c4b21f39f 100644 --- a/godoc/pres.go +++ b/godoc/pres.go @@ -40,6 +40,11 @@ type Presentation struct { ShowExamples bool DeclLinks bool + // SrcMode outputs source code instead of documentation in command-line mode. + SrcMode bool + // HTMLMode outputs HTML instead of plain text in command-line mode. + HTMLMode bool + // NotesRx optionally specifies a regexp to match // notes to render in the output. NotesRx *regexp.Regexp