blob: 3e475d8ce2e4b479d102fbd90d29aa8eedec9c33 [file] [edit]
// 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
}