registry.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. package commands
  2. import (
  3. "cmp"
  4. "flag"
  5. "fmt"
  6. "io"
  7. "maps"
  8. "slices"
  9. "strings"
  10. "sync"
  11. "github.com/anmitsu/go-shlex"
  12. )
  13. // Registry holds a set of commands.
  14. //
  15. // It is safe for concurrent use. Commands may be registered while surfaces are serving
  16. // from the registry: the ssh server clones it per session and the control socket filters
  17. // it per request, and either may overlap a Register.
  18. type Registry struct {
  19. mu sync.RWMutex
  20. commands map[Name]*Command
  21. // full is set on a filtered registry and points at the registry it was filtered
  22. // from, so a command that's filtered can be reported as such.
  23. full *Registry
  24. // reason explains, in the terms an operator cares about, why this registry withholds
  25. // a command they can name. It is supplied by whoever built the filter, because why a
  26. // surface declines a command is that surface's business, not the registry's.
  27. reason string
  28. // DefaultFormat decides how a result is rendered when the command line named no
  29. // format. It sees the command and its Result, and applies to values and refusals
  30. // alike: the registry does not special-case either. The renderer is shared between
  31. // surfaces; what each asks for by default is not, so the surface that owns this
  32. // registry sets it. Nil means text.
  33. DefaultFormat func(c *Command, r Result) Format
  34. }
  35. // defaultFormatFor resolves the surface's default for c's Result.
  36. func (r *Registry) defaultFormatFor(c *Command, res Result) Format {
  37. if r.DefaultFormat == nil {
  38. return FormatText
  39. }
  40. return r.DefaultFormat(c, res)
  41. }
  42. // New returns an empty Registry with only `help` available.
  43. func New() *Registry {
  44. r := &Registry{commands: map[Name]*Command{}}
  45. r.registerHelp()
  46. return r
  47. }
  48. func (r *Registry) registerHelp() {
  49. r.Register(&Command{
  50. Name: Help,
  51. ShortDescription: "prints available commands or help <command> for specific usage info",
  52. Run: func(a any, args []string) (Result, error) {
  53. if len(args) == 0 {
  54. return Value(r.Schema()), nil
  55. }
  56. if c := r.Get(Name(args[0])); c != nil {
  57. return Value(r.schemaFor(c)), nil
  58. }
  59. if err := r.unavailable(Name(args[0])); err != nil {
  60. return Failed("%s", err), nil
  61. }
  62. return Failed("Command not available %s", args[0]), nil
  63. },
  64. Text: renderHelp,
  65. })
  66. }
  67. // renderHelp is help's human rendering: the command list, or one command's usage.
  68. func renderHelp(v any, w io.Writer) error {
  69. switch h := v.(type) {
  70. case []CommandSchema:
  71. return writeCommandList(h, w)
  72. case CommandSchema:
  73. return WriteCommandHelp(h, w)
  74. default:
  75. return fmt.Errorf("expected a command schema, got %T", v)
  76. }
  77. }
  78. func writeCommandList(cmds []CommandSchema, w io.Writer) error {
  79. if _, err := io.WriteString(w, "Available commands:\n"); err != nil {
  80. return err
  81. }
  82. // In the order the schema came, which is the display order; see Registry.All.
  83. lines := make([]string, 0, len(cmds))
  84. for _, c := range cmds {
  85. lines = append(lines, fmt.Sprintf("%s - %s", c.Name, c.Short))
  86. }
  87. _, err := io.WriteString(w, strings.Join(lines, "\n")+"\n\n")
  88. return err
  89. }
  90. // WriteCommandHelp renders one command's usage. It is the single rendering of a
  91. // CommandSchema for a person: the daemon uses it for `help <command>` on both surfaces,
  92. // and a client that already holds the schema uses it for `-h`, so the two cannot drift.
  93. func WriteCommandHelp(c CommandSchema, w io.Writer) error {
  94. if _, err := fmt.Fprintf(w, "%s - %s\n", c.Name, c.Short); err != nil {
  95. return err
  96. }
  97. if c.Help != "" {
  98. if _, err := fmt.Fprintf(w, " %s\n", c.Help); err != nil {
  99. return err
  100. }
  101. }
  102. // Same layout as flag.PrintDefaults, which is what this used to be, including its
  103. // habit of leaving out a default that is the zero value.
  104. for _, f := range c.Flags {
  105. kind := ""
  106. if !f.Bool {
  107. kind = " string"
  108. }
  109. def := ""
  110. if f.Default != "" && f.Default != "false" {
  111. def = fmt.Sprintf(" (default %s)", f.Default)
  112. }
  113. if _, err := fmt.Fprintf(w, " -%s%s\n \t%s%s\n", f.Name, kind, f.Usage, def); err != nil {
  114. return err
  115. }
  116. }
  117. return nil
  118. }
  119. // Register adds a command that can be run by a user.
  120. //
  121. // It panics if the command declares a reserved flag. That is a programming error, and
  122. // surfacing it here, at startup, is better than at the moment an operator first types
  123. // the command.
  124. func (r *Registry) Register(c *Command) {
  125. if c.Flags != nil {
  126. if fl, _ := c.Flags(); fl != nil {
  127. for _, name := range reservedFlags {
  128. if fl.Lookup(name) != nil {
  129. panic(fmt.Sprintf("commands: %s declares reserved flag -%s", c.Name, name))
  130. }
  131. }
  132. }
  133. }
  134. r.insert(c)
  135. }
  136. // insert adds c without the reserved flag check, for commands that have already passed
  137. // it on another registry.
  138. func (r *Registry) insert(c *Command) {
  139. r.mu.Lock()
  140. defer r.mu.Unlock()
  141. r.commands[c.Name] = c
  142. }
  143. // Get returns the command registered under name, or nil if there is no such command.
  144. func (r *Registry) Get(name Name) *Command {
  145. r.mu.RLock()
  146. defer r.mu.RUnlock()
  147. return r.commands[name]
  148. }
  149. // Match returns the names of every command starting with prefix, sorted, as strings for
  150. // a prompt to complete and print.
  151. func (r *Registry) Match(prefix string) []string {
  152. cmds := make([]string, 0)
  153. r.mu.RLock()
  154. for n := range r.commands {
  155. if strings.HasPrefix(string(n), prefix) {
  156. cmds = append(cmds, string(n))
  157. }
  158. }
  159. r.mu.RUnlock()
  160. slices.Sort(cmds)
  161. return cmds
  162. }
  163. // All returns every registered command sorted by name, which is the order help lists
  164. // them and the schema describes them.
  165. func (r *Registry) All() []*Command {
  166. r.mu.RLock()
  167. defer r.mu.RUnlock()
  168. cmds := slices.Collect(maps.Values(r.commands))
  169. slices.SortFunc(cmds, func(a, b *Command) int { return cmp.Compare(a.Name, b.Name) })
  170. return cmds
  171. }
  172. // Clone returns a copy holding the same commands.
  173. //
  174. // help is re-registered on the copy rather than carried over, because the source's help
  175. // is bound to the source and would list its commands, not the copy's, once a caller
  176. // registers something on the copy.
  177. func (r *Registry) Clone() *Registry {
  178. r.mu.RLock()
  179. out := &Registry{
  180. commands: maps.Clone(r.commands),
  181. full: r.full,
  182. reason: r.reason,
  183. DefaultFormat: r.DefaultFormat,
  184. }
  185. r.mu.RUnlock()
  186. out.registerHelp()
  187. return out
  188. }
  189. // Filter returns a filtered copy of the registry.
  190. //
  191. // Keep should return true for commands that should be included in the copy.
  192. //
  193. // reason says, to an operator who names a command the copy left out, why this surface
  194. // does not serve it; "" leaves the registry to say only that it does not.
  195. func (r *Registry) Filter(keep func(*Command) bool, reason string) *Registry {
  196. out := &Registry{commands: map[Name]*Command{}, full: r, reason: reason, DefaultFormat: r.DefaultFormat}
  197. for _, cmd := range r.All() {
  198. if keep(cmd) {
  199. out.insert(cmd)
  200. }
  201. }
  202. // Bind help to the filtered set so it only advertises what this surface serves.
  203. out.registerHelp()
  204. return out
  205. }
  206. // Dispatch parses a command line and runs it, reporting problems to w. The error is for
  207. // the caller to log, not to show; see RunFunc and Failure.
  208. func (r *Registry) Dispatch(line string, w Output) error {
  209. // A line shlex cannot split, an unbalanced quote. Reported on Err so the output
  210. // stream stays parseable.
  211. args, err := shlex.Split(line, true)
  212. if err != nil {
  213. msg := "could not parse the command line: " + err.Error()
  214. if _, err := fmt.Fprintln(w.Err, msg); err != nil {
  215. return err
  216. }
  217. return &Failure{Msg: msg}
  218. }
  219. return r.DispatchArgs(args, w)
  220. }
  221. // DispatchArgs runs an already tokenized command line. The error is as for Dispatch.
  222. //
  223. // An empty line and an unknown command both end up running `help`, the first with the
  224. // arguments help itself would take; `<command> -h` is answered by the command's own
  225. // FlagSet, with the same rendering, in execLine.
  226. func (r *Registry) DispatchArgs(args []string, w Output) error {
  227. if len(args) == 0 {
  228. args = []string{"help"}
  229. }
  230. c := r.Get(Name(args[0]))
  231. if c == nil {
  232. if err := r.unavailable(Name(args[0])); err != nil {
  233. // Saying why, rather than "did not understand", keeps the rule legible and
  234. // points at a surface that will actually run the command.
  235. _, _ = fmt.Fprintln(w.Err, err)
  236. return err
  237. }
  238. // Only the first word was looked up, so only it is named.
  239. msg := fmt.Sprintf("did not understand: %s", args[0])
  240. if _, err := fmt.Fprintln(w.Out, msg); err != nil {
  241. return err
  242. }
  243. if err := r.DispatchArgs([]string{"help"}, w); err != nil {
  244. return err
  245. }
  246. return &Failure{Msg: msg}
  247. }
  248. return r.execLine(c, args[1:], w)
  249. }
  250. // unavailable explains why name cannot be run here when this registry was filtered from
  251. // one that holds it: the command exists, and this surface withholds it, for the reason
  252. // whoever built the filter gave. It is nil when name is simply unknown, so the two cases
  253. // read differently to an operator.
  254. func (r *Registry) unavailable(name Name) error {
  255. if r.full == nil {
  256. return nil
  257. }
  258. c := r.full.Get(name)
  259. if c == nil {
  260. return nil
  261. }
  262. reason := r.reason
  263. if reason == "" {
  264. reason = "this surface does not serve it"
  265. }
  266. return fmt.Errorf("%s is not available on this surface: %s", c.Name, reason)
  267. }
  268. // Resolve returns the command registered under name, or says why there is none. A
  269. // command this surface withholds is reported with the filter's reason, and one that
  270. // does not exist at all as unknown, so a client that misspells and one that asks for a
  271. // real command it cannot have here are told different things. It is the edge where a
  272. // name from the wire becomes a Command for a Request.
  273. func (r *Registry) Resolve(name Name) (*Command, error) {
  274. if c := r.Get(name); c != nil {
  275. return c, nil
  276. }
  277. if err := r.unavailable(name); err != nil {
  278. return nil, err
  279. }
  280. return nil, fmt.Errorf("unknown command %q", name)
  281. }
  282. // setParams sets each named parameter on fl, in the order of names, and stops at the
  283. // first that is unknown or does not parse. own says whether the command declared flags
  284. // of its own, which decides between "takes no parameters" and "has no parameter".
  285. //
  286. // The presentation flags are on fl, since commandFlags put them there, but they are not
  287. // parameters: a request has one place to choose its rendering, the envelope's format
  288. // field, and a client that sends them here is told where that is rather than having a
  289. // second, weaker way to say the same thing.
  290. func setParams(fl *flag.FlagSet, name string, names []string, params map[string]string, own bool) error {
  291. for _, k := range names {
  292. if isPresentationFlag(k) {
  293. return fmt.Errorf("%s is a command line flag, not a parameter; choose the rendering with the request's format field", k)
  294. }
  295. f := fl.Lookup(k)
  296. if f == nil {
  297. if !own {
  298. return fmt.Errorf("%s takes no parameters", name)
  299. }
  300. return fmt.Errorf("%s has no parameter %q", name, k)
  301. }
  302. v := params[k]
  303. if err := fl.Set(k, v); err != nil {
  304. if IsBoolFlag(f) {
  305. // The flag package's own words for this are "parse error".
  306. return fmt.Errorf("%s: expected a boolean, got %q", k, v)
  307. }
  308. return fmt.Errorf("%s: %w", k, err)
  309. }
  310. }
  311. return nil
  312. }
  313. // Schema describes every command in the registry, so a client can offer them without
  314. // having been built against this daemon.
  315. func (r *Registry) Schema() []CommandSchema {
  316. all := r.All()
  317. out := make([]CommandSchema, 0, len(all))
  318. for _, c := range all {
  319. out = append(out, r.schemaFor(c))
  320. }
  321. return out
  322. }
  323. // schemaFor describes one command. It is the one derivation, for the whole schema and
  324. // for `help <command>` alike.
  325. func (r *Registry) schemaFor(c *Command) CommandSchema {
  326. fl, _, _ := commandFlags(c)
  327. return schemaOf(c, fl)
  328. }
  329. // schemaOf describes c as driven through fl, the FlagSet commandFlags built for it. The
  330. // presentation flags are included because a command line client parses its line
  331. // against this and help renders from it; they are listed after the command's own,
  332. // which is where readers expect them. Invoke refuses them as parameters.
  333. func schemaOf(c *Command, fl *flag.FlagSet) CommandSchema {
  334. cs := CommandSchema{Name: c.Name, Short: c.ShortDescription, Help: c.Help}
  335. var own, shared []FlagSchema
  336. fl.VisitAll(func(f *flag.Flag) {
  337. fs := FlagSchema{
  338. Name: f.Name,
  339. Bool: IsBoolFlag(f),
  340. Default: f.DefValue,
  341. Usage: f.Usage,
  342. }
  343. if isPresentationFlag(f.Name) {
  344. shared = append(shared, fs)
  345. return
  346. }
  347. own = append(own, fs)
  348. })
  349. cs.Flags = append(own, shared...)
  350. return cs
  351. }
  352. // CommandSchema describes one command well enough for a client to offer it.
  353. type CommandSchema struct {
  354. Name Name `json:"name"`
  355. Short string `json:"short,omitempty"`
  356. Help string `json:"help,omitempty"`
  357. Flags []FlagSchema `json:"flags,omitempty"`
  358. }
  359. // FlagSchema describes one of a command's named parameters.
  360. type FlagSchema struct {
  361. Name string `json:"name"`
  362. // Bool reports a parameter that takes no value, which is what tells a command line
  363. // client whether `-pretty` swallows the word after it.
  364. Bool bool `json:"bool,omitempty"`
  365. Default string `json:"default,omitempty"`
  366. Usage string `json:"usage,omitempty"`
  367. }