├── go.mod ├── go.sum ├── README.md └── main.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mingrammer/gosearch 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/logrusorgru/aurora v0.0.0-20191116043053-66b7ad493a23 // indirect 7 | github.com/mingrammer/cfmt v1.1.0 8 | golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 2 | github.com/logrusorgru/aurora v0.0.0-20191116043053-66b7ad493a23 h1:Wp7NjqGKGN9te9N/rvXYRhlVcrulGdxnz8zadXWs7fc= 3 | github.com/logrusorgru/aurora v0.0.0-20191116043053-66b7ad493a23/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 4 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 5 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 6 | github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= 7 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 8 | github.com/mingrammer/cfmt v1.1.0 h1:fAALVQC+aa20fCvghuB5W6zBAAsGWKGdcZmexpPrvwo= 9 | github.com/mingrammer/cfmt v1.1.0/go.mod h1:Jqg1Lq43AMo3ggnIEpvIDbca1VSvdHDg0H13eDG+/ys= 10 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 11 | golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914 h1:MlY3mEfbnWGmUi4rtHOtNnnnN4UJRGSyLPx+DXA5Sq4= 12 | golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 13 | golang.org/x/sys v0.0.0-20180315095008-cc7307a45468/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 14 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 15 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoSearch 2 | 3 | Search the Go packages for [pkg.go.dev](https://pkg.go.dev/) via command-line. It supports all search options in [Search Help](https://pkg.go.dev/search-help). 4 | 5 | ## Installation 6 | 7 | ``` 8 | go get github.com/mingrammer/gosearch 9 | ``` 10 | 11 | ## Usage 12 | 13 | Basic search. 14 | 15 | ``` 16 | $ gosearch fastjson 17 | ``` 18 | 19 | You can query with multiple search words. 20 | 21 | ``` 22 | $ gosearch logging zero alloc 23 | ``` 24 | 25 | Use `-n` option to see more. (default: 10) 26 | 27 | ``` 28 | $ gosearch -n 20 redis 29 | ``` 30 | 31 | Use `-e` option to search for an exact match. 32 | 33 | ``` 34 | $ gosearch -e go cloud 35 | ``` 36 | 37 | Use `-o` option to combine searches. It will search for each word and combine their results. 38 | 39 | ``` 40 | $ gosearch -o json yaml 41 | ``` 42 | 43 | ## Example 44 | 45 | Search the mux packages with `gosearch mux`. 46 | 47 | ```shell 48 | $ gosearch mux 49 | github.com/gorilla/mux (v1.7.3) 50 | ├ Package mux implements a request router and dispatcher. 51 | └ Published: Jun 30, 2019 | Imported by: 6513 | License: BSD-3-Clause 52 | 53 | k8s.io/apiserver/pkg/server/mux (v0.0.0 (6eed2f5)) 54 | ├ Package mux contains abstractions for http multiplexing of APIs. 55 | └ Published: 1 day ago | Imported by: 222 | License: Apache-2.0 56 | 57 | github.com/containous/mux (v0.0.0 (c33f32e)) 58 | ├ Package mux implements a request router and dispatcher. 59 | └ Published: Oct 24, 2018 | Imported by: 95 | License: BSD-3-Clause 60 | 61 | k8s.io/kubernetes/pkg/genericapiserver/mux (v1.5.8) 62 | ├ Package mux contains abstractions for http multiplexing of APIs. 63 | └ Published: Sep 30, 2017 | Imported by: 61 | License: Apache-2.0 64 | 65 | k8s.io/kubernetes/pkg/genericapiserver/server/mux (v1.6.0 (alpha.1)) 66 | ├ Package mux contains abstractions for http multiplexing of APIs. 67 | └ Published: Jan 30, 2017 | Imported by: 48 | License: Apache-2.0 68 | 69 | github.com/coreos/etcd/third_party/github.com/gorilla/mux (v0.4.9) 70 | ├ Package gorilla/mux implements a request router and dispatcher. 71 | └ Published: Mar 31, 2015 | Imported by: 66 | License: BSD-3-Clause, Apache-2.0 72 | 73 | github.com/muxinc/mux-go/examples/common (v0.3.0) 74 | └ Published: Oct 25, 2019 | Imported by: 9 | License: MIT 75 | 76 | github.com/muxinc/mux-go (v0.3.0) 77 | └ Published: Oct 25, 2019 | Imported by: 12 | License: MIT 78 | 79 | github.com/yinqiwen/gsnova/common/mux (v0.30.0) 80 | └ Published: Oct 29, 2017 | Imported by: 26 | License: BSD-3-Clause 81 | 82 | v2ray.com/core/common/mux (v4.19.1+incompatible) 83 | └ Published: Jun 3, 2019 | Imported by: 24 | License: MIT 84 | ``` 85 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "math" 7 | "net/http" 8 | "sort" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/mingrammer/cfmt" 13 | "golang.org/x/net/html" 14 | ) 15 | 16 | const ( 17 | perPage = 10 18 | 19 | spClass = "SearchSnippet" 20 | hdClass = "SearchSnippet-header" 21 | snClass = "SearchSnippet-synopsis" 22 | ilClass = "SearchSnippet-infoLabel" 23 | ) 24 | 25 | type pkg struct { 26 | repo string 27 | desc string 28 | version string 29 | pubDate string 30 | importCnt string 31 | license string 32 | } 33 | 34 | type page struct { 35 | seq int 36 | pkgs []*pkg 37 | } 38 | type pages []*page 39 | 40 | func (p pages) Len() int { return len(p) } 41 | func (p pages) Less(i, j int) bool { return p[i].seq < p[j].seq } 42 | func (p pages) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 43 | 44 | func main() { 45 | count := flag.Int("n", 10, "the number of packages to search.") 46 | exact := flag.Bool("e", false, "search for an exact match.") 47 | useOR := flag.Bool("o", false, "combine searches. if true, query will be like 'yaml OR json'.") 48 | flag.Parse() 49 | 50 | // Build a query. 51 | glue := "+" 52 | if *useOR { // Put OR between each search query. 53 | glue = "+OR+" 54 | } 55 | query := strings.Join(flag.Args(), glue) 56 | if *exact { // Put a word or phrase inside quotes. 57 | query = "\"" + query + "\"" 58 | } 59 | pageN := int(math.Ceil(float64(*count) / perPage)) 60 | 61 | // Search the packages concurrently. 62 | pc := make(chan *page, pageN) 63 | wg := new(sync.WaitGroup) 64 | for n := 1; n < pageN+1; n++ { 65 | wg.Add(1) 66 | go search(query, n, pc, wg) 67 | } 68 | go func() { 69 | wg.Wait() 70 | close(pc) 71 | }() 72 | 73 | // Order by sequence. 74 | ps := make(pages, 0) 75 | for p := range pc { 76 | ps = append(ps, p) 77 | } 78 | sort.Sort(ps) 79 | 80 | // Print all found packages. 81 | for i, p := range ps { 82 | for j, pkg := range p.pkgs { 83 | if i*perPage+j >= *count { 84 | return 85 | } 86 | prettyPrint(pkg) 87 | } 88 | } 89 | } 90 | 91 | func search(query string, seq int, pc chan<- *page, wg *sync.WaitGroup) { 92 | defer wg.Done() 93 | 94 | baseURL := "https://pkg.go.dev/search" 95 | fullURL := fmt.Sprintf("%s?q=%s&page=%d", baseURL, query, seq) 96 | 97 | resp, err := http.Get(fullURL) 98 | if err != nil { 99 | panic(err) 100 | } 101 | defer resp.Body.Close() 102 | 103 | doc, err := html.Parse(resp.Body) 104 | if err != nil { 105 | panic(err) 106 | } 107 | 108 | pkgs := make([]*pkg, 0) 109 | spNodes := find(doc, condHasClass(spClass)) 110 | for _, spNode := range spNodes { 111 | hdNodes := find(spNode, condHasClass(hdClass)) 112 | pkgRepo := find(hdNodes[0], condValidTxt())[0] 113 | 114 | pkgDesc := "" 115 | snNodes := find(spNode, condHasClass(snClass)) 116 | txtNode := find(snNodes[0], condValidTxt()) 117 | if len(txtNode) > 0 { 118 | pkgDesc = txtNode[0].Data 119 | } 120 | 121 | ilNodes := find(spNode, condHasClass(ilClass)) 122 | pkgMeta := find(ilNodes[0], condValidTxt()) 123 | 124 | pkgs = append(pkgs, &pkg{ 125 | repo: strings.TrimSpace(pkgRepo.Data), 126 | desc: strings.TrimSpace(pkgDesc), 127 | version: strings.TrimSpace(pkgMeta[1].Data), 128 | pubDate: strings.TrimSpace(pkgMeta[3].Data), 129 | importCnt: strings.TrimSpace(pkgMeta[5].Data), 130 | license: strings.TrimSpace(pkgMeta[7].Data), 131 | }) 132 | } 133 | pc <- &page{seq, pkgs} 134 | } 135 | 136 | func find(node *html.Node, by cond) []*html.Node { 137 | nodes := make([]*html.Node, 0) 138 | for c := node.FirstChild; c != nil; c = c.NextSibling { 139 | if by(c) { 140 | nodes = append(nodes, c) 141 | } 142 | nodes = append(nodes, find(c, by)...) 143 | } 144 | return nodes 145 | } 146 | 147 | type cond func(*html.Node) bool 148 | 149 | func condHasClass(class string) cond { 150 | return func(node *html.Node) bool { 151 | for _, attr := range node.Attr { 152 | if attr.Key == "class" && attr.Val == class { 153 | return true 154 | } 155 | } 156 | return false 157 | } 158 | } 159 | 160 | func condValidTxt() cond { 161 | return func(node *html.Node) bool { 162 | return node.Type == html.TextNode && strings.TrimSpace(node.Data) != "" && node.Data != "|" 163 | } 164 | } 165 | 166 | func prettyPrint(p *pkg) { 167 | fmt.Printf("%s (%s)\n", cfmt.Ssuccess(p.repo), cfmt.Sinfo(p.version)) 168 | if p.desc != "" { 169 | fmt.Printf("├ %s\n", p.desc) 170 | } 171 | fmt.Printf("└ Published: %s | Imported by: %s | License: %s\n\n", p.pubDate, p.importCnt, p.license) 172 | } 173 | --------------------------------------------------------------------------------