| // Copyright 2026 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 main |
| |
| import ( |
| "flag" |
| "fmt" |
| "io" |
| "os" |
| "path/filepath" |
| "runtime/debug" |
| "slices" |
| "strings" |
| "time" |
| |
| "golang.org/x/telemetry/counter" |
| ) |
| |
| // A command describes a subcommand of the tool. |
| type command struct { |
| name string // subcommand name |
| args string // e.g. "<package>[@version]"; empty for no-arg commands |
| summary string // one-line description |
| description string // detailed description |
| flags *flag.FlagSet |
| run func(fs *flag.FlagSet, stdout, stderr io.Writer) int |
| } |
| |
| func (c *command) usageLine() string { |
| parts := []string{filepath.Base(os.Args[0]), c.name} |
| if c.args != "" { |
| parts = append(parts, "[flags]", c.args) |
| } |
| return strings.Join(parts, " ") |
| } |
| |
| // printCommandUsage writes usage for a single command to w. |
| func printCommandUsage(w io.Writer, c *command) { |
| fmt.Fprintf(w, "Usage: %s\n", c.usageLine()) |
| fmt.Fprintf(w, "\n%s\n", c.summary) |
| if c.description != "" { |
| fmt.Fprintf(w, "\n%s\n", c.description) |
| } |
| if c.flags != nil { |
| fmt.Fprintln(w) |
| fmt.Fprintln(w, "Flags:") |
| c.flags.SetOutput(w) |
| c.flags.PrintDefaults() |
| } |
| } |
| |
| // printUsage writes usage for all commands to w. |
| func printUsage(w io.Writer, cmds []*command) { |
| fmt.Fprintf(w, "%s queries the pkg.go.dev API for information about Go packages and modules.\n\n", filepath.Base(os.Args[0])) |
| fmt.Fprintln(w, "Usage:") |
| for _, c := range cmds { |
| line := c.usageLine() |
| fmt.Fprintf(w, " %-50s %s\n", line, c.summary) |
| } |
| fmt.Fprintf(w, "\nRun \"%s <command> -h\" for details on available flags for each command.\n", filepath.Base(os.Args[0])) |
| } |
| |
| // dispatch finds and runs the matching command. It returns the exit code. |
| func dispatch(args []string, cmds []*command, stdout, stderr io.Writer) int { |
| if len(args) == 0 { |
| printUsage(stderr, cmds) |
| return 2 |
| } |
| |
| // Bare "-h" with no positional arg: show overview. |
| hasPositional := false |
| for _, a := range args { |
| if !strings.HasPrefix(a, "-") { |
| hasPositional = true |
| break |
| } |
| } |
| if !hasPositional { |
| printUsage(stderr, cmds) |
| for _, a := range args { |
| if a == "-h" || a == "-help" || a == "--help" { |
| return 0 |
| } |
| } |
| return 2 |
| } |
| |
| named := map[string]*command{} |
| for _, c := range cmds { |
| named[c.name] = c |
| } |
| |
| // Scan for the first positional arg to determine the subcommand. |
| for i, a := range args { |
| if strings.HasPrefix(a, "-") { |
| continue |
| } |
| if c, ok := named[a]; ok { |
| rest := slices.Delete(args, i, i+1) |
| return parseAndRun(c, rest, stdout, stderr) |
| } |
| fmt.Fprintf(stderr, "unknown command: %s\n", a) |
| printUsage(stderr, cmds) |
| return 2 |
| } |
| |
| printUsage(stderr, cmds) |
| return 2 |
| } |
| |
| func parseAndRun(c *command, args []string, stdout, stderr io.Writer) int { |
| if c.flags == nil { |
| // No-arg command (help, version). |
| for _, a := range args { |
| if a == "-h" || a == "-help" || a == "--help" { |
| printCommandUsage(stderr, c) |
| return 0 |
| } |
| } |
| return c.run(nil, stdout, stderr) |
| } |
| c.flags.SetOutput(stderr) |
| c.flags.Usage = func() { printCommandUsage(stderr, c) } |
| // TODO: Consider supporting flags after positional arguments for better UX. |
| // Currently, flags must appear before positional arguments. |
| // Works: pkgsite-cli package -doc=text -examples -imports -json -module golang.org/x/tools golang.org/x/tools/go/packages |
| // Fails: pkgsite-cli package golang.org/x/tools/go/packages -doc=text -examples -imports -json -module golang.org/x/tools |
| if err := c.flags.Parse(args); err != nil { |
| if err == flag.ErrHelp { |
| return 0 |
| } |
| return 2 |
| } |
| |
| counter.New("pkgsite-cli/command:" + c.name).Inc() |
| |
| return c.run(c.flags, stdout, stderr) |
| } |
| |
| func versionInfo() string { |
| bi, ok := debug.ReadBuildInfo() |
| if !ok { |
| return filepath.Base(os.Args[0]) + " (unknown version)" |
| } |
| v := bi.Main.Version |
| if v == "" || v == "(devel)" { |
| const vcsRevisionLen = 12 |
| v = "devel" |
| for _, s := range bi.Settings { |
| if s.Key == "vcs.revision" && len(s.Value) >= vcsRevisionLen { |
| v += " " + s.Value[:vcsRevisionLen] |
| } |
| } |
| } |
| return filepath.Base(os.Args[0]) + " " + v + " " + bi.GoVersion |
| } |
| |
| const defaultServer = "https://pkg.go.dev" |
| |
| // commonFlags are shared across all subcommands. |
| type commonFlags struct { |
| jsonOut bool |
| limit int |
| server string |
| timeout time.Duration |
| printURLs bool |
| } |
| |
| func (f *commonFlags) register(fs *flag.FlagSet) { |
| fs.BoolVar(&f.jsonOut, "json", false, "output JSON") |
| fs.IntVar(&f.limit, "limit", 0, "max results (default: 25)") |
| fs.StringVar(&f.server, "server", defaultServer, "API server URL") |
| fs.DurationVar(&f.timeout, "timeout", 30*time.Second, "request timeout") |
| fs.BoolVar(&f.printURLs, "x", false, "print URL before fetching") |
| } |
| |
| func (f *commonFlags) effectiveLimit() int { |
| if f.limit > 0 { |
| return f.limit |
| } |
| return 25 |
| } |