1
0

command.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. package commands
  2. import (
  3. "errors"
  4. "flag"
  5. "fmt"
  6. "io"
  7. "strings"
  8. )
  9. // CommandFlags is called before help or command execution to parse command line flags.
  10. // It should return a flag.FlagSet instance and a pointer to the struct that will contain parsed flags
  11. //
  12. // Every call must build a fresh FlagSet and a fresh struct. Nothing may be shared between
  13. // calls: a command runs on more than one surface and on more than one goroutine.
  14. type CommandFlags func() (*flag.FlagSet, any)
  15. // RunFunc computes a command's Result.
  16. //
  17. // fs is the pointer returned by Command.Flags, if there was one; -h, -help, -json and
  18. // -pretty are reserved and handled before Run is called. a is the operands: whatever
  19. // was not consumed as a flag. The Result is rendered by whichever surface asked: Value
  20. // for a command that reports, Did or Failed for one that acts, Nothing for one with
  21. // nothing to say. An error is for a failure of the command itself; a bad argument is
  22. // Failed, so the caller hears why in its own format.
  23. type RunFunc func(fs any, a []string) (Result, error)
  24. // TextFunc renders a Result's Value for a person. See Typed for declaring one against
  25. // the type the command actually returns.
  26. type TextFunc func(v any, w io.Writer) error
  27. type Command struct {
  28. Name Name
  29. ShortDescription string
  30. Help string
  31. ForProfiling bool
  32. Flags CommandFlags
  33. // Run produces the command's Result, leaving presentation to Render.
  34. Run RunFunc
  35. // Text renders a Result's Value for a person. Both surfaces call it, so a command
  36. // reads the same over ssh as it does through the control socket. It never sees an
  37. // outcome: Render prints those itself, the same way for every command. Leaving it
  38. // nil declares that no human rendering of the Value exists yet, which JSONOnly says
  39. // up front; Render then refuses text rather than inventing an interim form for
  40. // people to script against.
  41. Text TextFunc
  42. // JSONOnly declares that the command's Value has no human rendering yet, so a surface
  43. // whose default is text asks for JSON instead, and the schema says so to a client. It
  44. // is a statement that no such form exists, not a fallback. A refusal is still
  45. // rendered as a sentence, since that always has a human form.
  46. JSONOnly bool
  47. // AfterReply runs once the surface has delivered Run's result, and only then. It is
  48. // for a command whose action would cut off its own reply: reload restarts the ssh
  49. // server, which ends the session that asked, so the sentence Run returns has to be
  50. // on the wire before the reload begins. Run promises; AfterReply acts.
  51. AfterReply func()
  52. }
  53. // Failure is the error a surface gets for a refusal the person has already been shown:
  54. // a Run that returned Failed, a command that is not there, a line that would not parse.
  55. // The message it rendered is the whole story, so the surface reports the failure, exit
  56. // status 1 over ssh, and does not log it.
  57. type Failure struct {
  58. Msg string
  59. }
  60. func (f *Failure) Error() string {
  61. return f.Msg
  62. }
  63. // commandFlags returns the FlagSet a command is driven through, on any surface: the
  64. // command's own flags, if it declares any, with the two presentation flags added. own
  65. // reports whether the command declared any of its own, which is what tells a caller
  66. // whether to say a parameter is unknown or that the command takes none at all.
  67. //
  68. // The presentation flags are defined here rather than by each command, because how a
  69. // result is displayed is not a property of the command that produced it. This is also
  70. // the one definition of them: the ssh line parses into it, nebula ctl parses its line
  71. // against the schema derived from it, and help renders from that schema. They are
  72. // command line flags only: a socket request chooses its rendering with the envelope's
  73. // format field, and Invoke refuses them as parameters.
  74. func commandFlags(c *Command) (fl *flag.FlagSet, fs any, own bool) {
  75. if c.Flags != nil {
  76. fl, fs = c.Flags()
  77. }
  78. own = fl != nil
  79. if fl == nil {
  80. fl = flag.NewFlagSet(string(c.Name), flag.ContinueOnError)
  81. }
  82. addPresentationFlags(fl)
  83. // What Parse prints after a bad flag, and for -h: the command's usage as help renders
  84. // it, so a line that went wrong is answered with the same text as `help <command>`.
  85. fl.Usage = func() { _ = WriteCommandHelp(schemaOf(c, fl), fl.Output()) }
  86. return fl, fs, own
  87. }
  88. // addPresentationFlags defines the two presentation flags on fl.
  89. func addPresentationFlags(fl *flag.FlagSet) {
  90. fl.Bool(FlagJSON, false, "outputs as json")
  91. fl.Bool(FlagPretty, false, "pretty prints json, assumes -json")
  92. }
  93. // Flagged fills in c's Flags and Run as a pair: define declares the flags on a fresh F,
  94. // and run is handed that F. The struct the flags fill is the struct Run reads by
  95. // construction, so no Run has to assert the type it was given or decide what to do
  96. // when the assertion fails.
  97. func Flagged[F any](c *Command, define func(*flag.FlagSet, *F), run func(*F, []string) (Result, error)) *Command {
  98. c.Flags = func() (*flag.FlagSet, any) {
  99. fl := flag.NewFlagSet("", flag.ContinueOnError)
  100. s := new(F)
  101. define(fl, s)
  102. return fl, s
  103. }
  104. c.Run = func(fs any, a []string) (Result, error) {
  105. s, ok := fs.(*F)
  106. if !ok {
  107. return Result{}, fmt.Errorf("%s was handed %T for its flags, not %T", c.Name, fs, s)
  108. }
  109. return run(s, a)
  110. }
  111. return c
  112. }
  113. // isPresentationFlag reports whether name is one of the two flags commandFlags adds.
  114. func isPresentationFlag(name string) bool {
  115. return name == FlagJSON || name == FlagPretty
  116. }
  117. // execLine runs a command line against c, reporting problems to w. It is the path from
  118. // an ssh line to output: the line is parsed against the command's own FlagSet and Run
  119. // reads that FlagSet's struct directly. Nothing is serialised and parsed again on the
  120. // way, so a flag.Value whose String does not re-parse, flag.Func above all, is applied
  121. // as the line said and not as its String says. The control socket fills the same
  122. // FlagSet through Set in Invoke; Parse and Set are the flag package's one parser.
  123. func (r *Registry) execLine(c *Command, args []string, w Output) error {
  124. fl, fs, _ := commandFlags(c)
  125. // Parse reports a bad flag to this writer, with the command's usage after it. -h,
  126. // -help and --help take the same path with nothing wrong: the usage is the whole
  127. // answer, and Parse says so with ErrHelp. A -h that is a flag's value or follows --
  128. // is read as the operand it is, since ParseArgs is the one reading of the line.
  129. fl.SetOutput(w.Err)
  130. operands, err := ParseArgs(fl, args)
  131. if err != nil {
  132. if errors.Is(err, flag.ErrHelp) {
  133. return nil
  134. }
  135. return err
  136. }
  137. format := presentationFormat(fl)
  138. res, err := c.Run(fs, operands)
  139. if err != nil {
  140. return err
  141. }
  142. if res.Empty() {
  143. runAfterReply(c)
  144. return nil
  145. }
  146. // The line wins when it asked for a format. Otherwise the surface that owns the
  147. // registry decides, from the command and its Result, which is how ssh keeps printing
  148. // what it always has while another surface can choose differently.
  149. f := format
  150. if f == FormatUnset {
  151. f = r.defaultFormatFor(c, res)
  152. }
  153. if err := Render(c, res, f, w.Out); err != nil {
  154. if errors.Is(err, ErrNoTextRendering) {
  155. _, _ = fmt.Fprintln(w.Err, err.Error())
  156. }
  157. return err
  158. }
  159. runAfterReply(c)
  160. if !res.OK {
  161. return &Failure{Msg: res.Reason}
  162. }
  163. return nil
  164. }
  165. // runAfterReply performs the command's deferred action, if it has one. It is called only
  166. // once the reply has been written, which is the whole point of AfterReply.
  167. func runAfterReply(c *Command) {
  168. if c.AfterReply != nil {
  169. c.AfterReply()
  170. }
  171. }
  172. // ParseArgs parses args with fl and returns the operands.
  173. //
  174. // It differs from fl.Parse in two ways, and both surfaces parse through here so a line
  175. // means the same typed at an ssh prompt or to nebula ctl.
  176. //
  177. // Flags and operands may come in any order, which is the one way it differs from
  178. // fl.Parse. Parse stops at the first operand, so `print-tunnel 10.0.0.1 -json` would
  179. // reach the command with "-json" as a second operand and the flag silently dropped. Here
  180. // an operand ends nothing: parsing resumes after it, and only "--" ends the flags, with
  181. // everything after it an operand. What counts as a flag is Parse's rule: every argument
  182. // that starts with "-", a negative number included, so one that is meant as an operand
  183. // goes after "--".
  184. func ParseArgs(fl *flag.FlagSet, args []string) ([]string, error) {
  185. operands := make([]string, 0, len(args))
  186. for len(args) > 0 {
  187. // Walk the flags the way Parse will, to the first argument it would not read as a
  188. // flag: an operand or the terminator. Everything before it is Parse's.
  189. end := len(args)
  190. for i := 0; i < len(args); {
  191. a := args[i]
  192. if a == "--" {
  193. if err := fl.Parse(args[:i]); err != nil {
  194. return nil, err
  195. }
  196. return append(operands, args[i+1:]...), nil
  197. }
  198. if len(a) < 2 || a[0] != '-' {
  199. end = i
  200. break
  201. }
  202. f, hasValue := LookupArg(fl, a)
  203. if f == nil {
  204. // An undefined flag: leave it for Parse to report.
  205. i++
  206. continue
  207. }
  208. if hasValue || IsBoolFlag(f) {
  209. i++
  210. } else {
  211. // The flag's value is the next argument, whatever it looks like.
  212. i += 2
  213. }
  214. }
  215. if err := fl.Parse(args[:end]); err != nil {
  216. return nil, err
  217. }
  218. if end == len(args) {
  219. break
  220. }
  221. operands = append(operands, args[end])
  222. args = args[end+1:]
  223. }
  224. return operands, nil
  225. }
  226. // LookupArg reads arg the way Parse does: one or two leading dashes, the flag's name, and
  227. // an optional "=value". f is the flag of fl by that name, nil when fl has none, and
  228. // hasValue reports that the value came with the argument rather than in the one after it.
  229. // It is the one reading of an argument's shape: ParseArgs walks a line with it, and
  230. // nebula ctl lifts its own flags out of a line with it, so the two cannot disagree about
  231. // which word is a flag's value.
  232. func LookupArg(fl *flag.FlagSet, arg string) (f *flag.Flag, hasValue bool) {
  233. name := strings.TrimPrefix(strings.TrimPrefix(arg, "-"), "-")
  234. if eq := strings.IndexByte(name, '='); eq >= 0 {
  235. name, hasValue = name[:eq], true
  236. }
  237. return fl.Lookup(name), hasValue
  238. }
  239. // IsBoolFlag reports whether f is satisfied by its presence alone and never reads the
  240. // argument after it as its value. The flag package marks these with an unexported
  241. // interface, which is the only way to tell.
  242. func IsBoolFlag(f *flag.Flag) bool {
  243. bf, ok := f.Value.(interface{ IsBoolFlag() bool })
  244. return ok && bf.IsBoolFlag()
  245. }