registry.go 14 KB

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