| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430 |
- package commands
- import (
- "cmp"
- "flag"
- "fmt"
- "io"
- "maps"
- "slices"
- "strings"
- "sync"
- "github.com/anmitsu/go-shlex"
- )
- // Registry holds a set of commands.
- //
- // It is safe for concurrent use. Commands may be registered while surfaces are serving
- // from the registry: the ssh server clones it per session and the control socket filters
- // it per request, and either may overlap a Register.
- type Registry struct {
- mu sync.RWMutex
- commands map[Name]*Command
- // full is set on a filtered registry and points at the registry it was filtered
- // from, so a command that's filtered can be reported as such.
- full *Registry
- // reason explains, in the terms an operator cares about, why this registry withholds
- // a command they can name. It is supplied by whoever built the filter, because why a
- // surface declines a command is that surface's business, not the registry's.
- reason string
- // DefaultFormat decides how a result is rendered when the command line named no
- // format. It sees the command and its Result, and applies to values and refusals
- // alike: the registry does not special-case either. The renderer is shared between
- // surfaces; what each asks for by default is not, so the surface that owns this
- // registry sets it. Nil means text.
- DefaultFormat func(c *Command, r Result) Format
- }
- // defaultFormatFor resolves the surface's default for c's Result.
- func (r *Registry) defaultFormatFor(c *Command, res Result) Format {
- if r.DefaultFormat == nil {
- return FormatText
- }
- return r.DefaultFormat(c, res)
- }
- // New returns an empty Registry with only `help` available.
- func New() *Registry {
- r := &Registry{commands: map[Name]*Command{}}
- r.registerHelp()
- return r
- }
- func (r *Registry) registerHelp() {
- r.Register(&Command{
- Name: Help,
- ShortDescription: "prints available commands or help <command> for specific usage info",
- Run: func(a any, args []string) (Result, error) {
- if len(args) == 0 {
- return Value(r.Schema()), nil
- }
- if c := r.Get(Name(args[0])); c != nil {
- return Value(r.schemaFor(c)), nil
- }
- if err := r.unavailable(Name(args[0])); err != nil {
- return Failed("%s", err), nil
- }
- return Failed("Command not available %s", args[0]), nil
- },
- Text: renderHelp,
- })
- }
- // renderHelp is help's human rendering: the command list, or one command's usage.
- func renderHelp(v any, w io.Writer) error {
- switch h := v.(type) {
- case []CommandSchema:
- return writeCommandList(h, w)
- case CommandSchema:
- return WriteCommandHelp(h, w)
- default:
- return fmt.Errorf("expected a command schema, got %T", v)
- }
- }
- func writeCommandList(cmds []CommandSchema, w io.Writer) error {
- if _, err := io.WriteString(w, "Available commands:\n"); err != nil {
- return err
- }
- // In the order the schema came, which is the display order; see Registry.All.
- lines := make([]string, 0, len(cmds))
- for _, c := range cmds {
- lines = append(lines, fmt.Sprintf("%s - %s", c.Name, c.Short))
- }
- _, err := io.WriteString(w, strings.Join(lines, "\n")+"\n\n")
- return err
- }
- // WriteCommandHelp renders one command's usage. It is the single rendering of a
- // CommandSchema for a person: the daemon uses it for `help <command>` on both surfaces,
- // and a client that already holds the schema uses it for `-h`, so the two cannot drift.
- func WriteCommandHelp(c CommandSchema, w io.Writer) error {
- if _, err := fmt.Fprintf(w, "%s - %s\n", c.Name, c.Short); err != nil {
- return err
- }
- if c.Help != "" {
- if _, err := fmt.Fprintf(w, " %s\n", c.Help); err != nil {
- return err
- }
- }
- // Same layout as flag.PrintDefaults, which is what this used to be, including its
- // habit of leaving out a default that is the zero value.
- for _, f := range c.Flags {
- kind := ""
- if !f.Bool {
- kind = " string"
- }
- def := ""
- if f.Default != "" && f.Default != "false" {
- def = fmt.Sprintf(" (default %s)", f.Default)
- }
- if _, err := fmt.Fprintf(w, " -%s%s\n \t%s%s\n", f.Name, kind, f.Usage, def); err != nil {
- return err
- }
- }
- return nil
- }
- // Register adds a command that can be run by a user.
- //
- // It panics if the command declares a reserved flag. That is a programming error, and
- // surfacing it here, at startup, is better than at the moment an operator first types
- // the command.
- func (r *Registry) Register(c *Command) {
- if c.Run == nil {
- panic(fmt.Sprintf("commands: %s has no Run", c.Name))
- }
- if c.Flags != nil {
- if fl, _ := c.Flags(); fl != nil {
- for _, name := range reservedFlags {
- if fl.Lookup(name) != nil {
- panic(fmt.Sprintf("commands: %s declares reserved flag -%s", c.Name, name))
- }
- }
- }
- }
- r.insert(c)
- }
- // insert adds c without the reserved flag check, for commands that have already passed
- // it on another registry.
- func (r *Registry) insert(c *Command) {
- r.mu.Lock()
- defer r.mu.Unlock()
- r.commands[c.Name] = c
- }
- // Get returns the command registered under name, or nil if there is no such command.
- func (r *Registry) Get(name Name) *Command {
- r.mu.RLock()
- defer r.mu.RUnlock()
- return r.commands[name]
- }
- // Match returns the names of every command starting with prefix, sorted, as strings for
- // a prompt to complete and print.
- func (r *Registry) Match(prefix string) []string {
- cmds := make([]string, 0)
- r.mu.RLock()
- for n := range r.commands {
- if strings.HasPrefix(string(n), prefix) {
- cmds = append(cmds, string(n))
- }
- }
- r.mu.RUnlock()
- slices.Sort(cmds)
- return cmds
- }
- // All returns every registered command sorted by name, which is the order help lists
- // them and the schema describes them.
- func (r *Registry) All() []*Command {
- r.mu.RLock()
- defer r.mu.RUnlock()
- cmds := slices.Collect(maps.Values(r.commands))
- slices.SortFunc(cmds, func(a, b *Command) int { return cmp.Compare(a.Name, b.Name) })
- return cmds
- }
- // Clone returns a copy holding the same commands.
- //
- // help is re-registered on the copy rather than carried over, because the source's help
- // is bound to the source and would list its commands, not the copy's, once a caller
- // registers something on the copy.
- func (r *Registry) Clone() *Registry {
- r.mu.RLock()
- out := &Registry{
- commands: maps.Clone(r.commands),
- full: r.full,
- reason: r.reason,
- DefaultFormat: r.DefaultFormat,
- }
- r.mu.RUnlock()
- out.registerHelp()
- return out
- }
- // Filter returns a filtered copy of the registry.
- //
- // Keep should return true for commands that should be included in the copy.
- //
- // reason says, to an operator who names a command the copy left out, why this surface
- // does not serve it; "" leaves the registry to say only that it does not.
- func (r *Registry) Filter(keep func(*Command) bool, reason string) *Registry {
- out := &Registry{commands: map[Name]*Command{}, full: r, reason: reason, DefaultFormat: r.DefaultFormat}
- for _, cmd := range r.All() {
- if keep(cmd) {
- out.insert(cmd)
- }
- }
- // Bind help to the filtered set so it only advertises what this surface serves.
- out.registerHelp()
- return out
- }
- // Dispatch parses a command line and runs it, reporting problems to w. The error is for
- // the caller to log, not to show; see RunFunc and Failure.
- func (r *Registry) Dispatch(line string, w Output) error {
- // A line shlex cannot split, an unbalanced quote. Reported on Err so the output
- // stream stays parseable.
- args, err := shlex.Split(line, true)
- if err != nil {
- msg := "could not parse the command line: " + err.Error()
- if _, err := fmt.Fprintln(w.Err, msg); err != nil {
- return err
- }
- return &Failure{Msg: msg}
- }
- return r.DispatchArgs(args, w)
- }
- // DispatchArgs runs an already tokenized command line. The error is as for Dispatch.
- //
- // An empty line and an unknown command both end up running `help`, the first with the
- // arguments help itself would take; `<command> -h` is answered by the command's own
- // FlagSet, with the same rendering, in execLine.
- func (r *Registry) DispatchArgs(args []string, w Output) error {
- if len(args) == 0 {
- args = []string{"help"}
- }
- c := r.Get(Name(args[0]))
- if c == nil {
- if err := r.unavailable(Name(args[0])); err != nil {
- // Saying why, rather than "did not understand", keeps the rule legible and
- // points at a surface that will actually run the command.
- _, _ = fmt.Fprintln(w.Err, err)
- return err
- }
- // Only the first word was looked up, so only it is named.
- msg := fmt.Sprintf("did not understand: %s", args[0])
- if _, err := fmt.Fprintln(w.Out, msg); err != nil {
- return err
- }
- if err := r.DispatchArgs([]string{"help"}, w); err != nil {
- return err
- }
- return &Failure{Msg: msg}
- }
- return r.execLine(c, args[1:], w)
- }
- // unavailable explains why name cannot be run here when this registry was filtered from
- // one that holds it: the command exists, and this surface withholds it, for the reason
- // whoever built the filter gave. It is nil when name is simply unknown, so the two cases
- // read differently to an operator.
- func (r *Registry) unavailable(name Name) error {
- if r.full == nil {
- return nil
- }
- c := r.full.Get(name)
- if c == nil {
- return nil
- }
- reason := r.reason
- if reason == "" {
- reason = "this surface does not serve it"
- }
- return fmt.Errorf("%s is not available on this surface: %s", c.Name, reason)
- }
- // Resolve returns the command registered under name, or says why there is none. A
- // command this surface withholds is reported with the filter's reason, and one that
- // does not exist at all as unknown, so a client that misspells and one that asks for a
- // real command it cannot have here are told different things. It is the edge where a
- // name from the wire becomes a Command for a Request.
- func (r *Registry) Resolve(name Name) (*Command, error) {
- if c := r.Get(name); c != nil {
- return c, nil
- }
- if err := r.unavailable(name); err != nil {
- return nil, err
- }
- return nil, fmt.Errorf("unknown command %q", name)
- }
- // setParams sets each named parameter on fl, in the order of names, and stops at the
- // first that is unknown or does not parse. own says whether the command declared flags
- // of its own, which decides between "takes no parameters" and "has no parameter".
- //
- // The presentation flags are on fl, since commandFlags put them there, but they are not
- // parameters: a request has one place to choose its rendering, the envelope's format
- // field, and a client that sends them here is told where that is rather than having a
- // second, weaker way to say the same thing.
- func setParams(fl *flag.FlagSet, name string, names []string, params map[string]string, own bool) error {
- for _, k := range names {
- if isPresentationFlag(k) {
- return fmt.Errorf("%s is a command line flag, not a parameter; choose the rendering with the request's format field", k)
- }
- f := fl.Lookup(k)
- if f == nil {
- if !own {
- return fmt.Errorf("%s takes no parameters", name)
- }
- return fmt.Errorf("%s has no parameter %q", name, k)
- }
- v := params[k]
- if err := fl.Set(k, v); err != nil {
- if IsBoolFlag(f) {
- // The flag package's own words for this are "parse error".
- return fmt.Errorf("%s: expected a boolean, got %q", k, v)
- }
- return fmt.Errorf("%s: %w", k, err)
- }
- }
- return nil
- }
- // Schema describes every command in the registry, so a client can offer them without
- // having been built against this daemon.
- func (r *Registry) Schema() []CommandSchema {
- all := r.All()
- out := make([]CommandSchema, 0, len(all))
- for _, c := range all {
- out = append(out, r.schemaFor(c))
- }
- return out
- }
- // schemaFor describes one command. It is the one derivation, for the whole schema and
- // for `help <command>` alike.
- func (r *Registry) schemaFor(c *Command) CommandSchema {
- fl, _, _ := commandFlags(c)
- return schemaOf(c, fl)
- }
- // schemaOf describes c as driven through fl, the FlagSet commandFlags built for it. The
- // presentation flags are included because a command line client parses its line
- // against this and help renders from it; they are listed after the command's own,
- // which is where readers expect them. Invoke refuses them as parameters.
- func schemaOf(c *Command, fl *flag.FlagSet) CommandSchema {
- cs := CommandSchema{Name: c.Name, Short: c.ShortDescription, Help: c.Help}
- var own, shared []FlagSchema
- fl.VisitAll(func(f *flag.Flag) {
- fs := FlagSchema{
- Name: f.Name,
- Bool: IsBoolFlag(f),
- Default: f.DefValue,
- Usage: f.Usage,
- }
- if isPresentationFlag(f.Name) {
- shared = append(shared, fs)
- return
- }
- own = append(own, fs)
- })
- cs.Flags = append(own, shared...)
- return cs
- }
- // CommandSchema describes one command well enough for a client to offer it.
- type CommandSchema struct {
- Name Name `json:"name"`
- Short string `json:"short,omitempty"`
- Help string `json:"help,omitempty"`
- Flags []FlagSchema `json:"flags,omitempty"`
- }
- // FlagSchema describes one of a command's named parameters.
- type FlagSchema struct {
- Name string `json:"name"`
- // Bool reports a parameter that takes no value, which is what tells a command line
- // client whether `-pretty` swallows the word after it.
- Bool bool `json:"bool,omitempty"`
- Default string `json:"default,omitempty"`
- Usage string `json:"usage,omitempty"`
- }
|