registry.go 14 KB

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