completor.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package promputils
  15. import (
  16. "bytes"
  17. "fmt"
  18. "regexp"
  19. "strings"
  20. prompt "github.com/c-bata/go-prompt"
  21. "yunion.io/x/structarg"
  22. )
  23. var (
  24. subcmds = make(map[string]*Cmd)
  25. rootCmd *Cmd
  26. )
  27. var optionHelp = []prompt.Suggest{
  28. {Text: "-h"},
  29. {Text: "--help"},
  30. }
  31. type Cmd struct {
  32. Name string
  33. Desc string
  34. optArgs []CmdArgument
  35. posArgs []CmdArgument
  36. ParentCmd *Cmd
  37. SubCmds []*Cmd
  38. }
  39. func InitRootCmd(name, desc string, optArgs, posArgs []structarg.Argument) *Cmd {
  40. rootCmd = NewCmd(name, desc)
  41. for _, optA := range optArgs {
  42. rootCmd.AddOptArgument(optA, optA.Token(), optA.HelpString(""))
  43. }
  44. for _, posA := range posArgs {
  45. rootCmd.AddPosArgument(posA, posA.Token(), posA.HelpString(""))
  46. }
  47. return rootCmd
  48. }
  49. func GetRootCmd() *Cmd {
  50. return rootCmd
  51. }
  52. func NewCmd(name, desc string) *Cmd {
  53. return &Cmd{
  54. Name: name,
  55. Desc: desc,
  56. optArgs: make([]CmdArgument, 0),
  57. posArgs: make([]CmdArgument, 0),
  58. SubCmds: make([]*Cmd, 0),
  59. }
  60. }
  61. func (c Cmd) getPromptSuggests(args []CmdArgument) []prompt.Suggest {
  62. ret := make([]prompt.Suggest, 0)
  63. for _, arg := range args {
  64. ret = append(ret, arg.Suggest)
  65. }
  66. return ret
  67. }
  68. func (c *Cmd) Root() *Cmd {
  69. if c.ParentCmd == nil {
  70. return c
  71. }
  72. return c.ParentCmd.Root()
  73. }
  74. func (c Cmd) GetName() string {
  75. return c.Name
  76. }
  77. func (c *Cmd) AddCmd(cmd *Cmd) {
  78. c.SubCmds = append(c.SubCmds, cmd)
  79. }
  80. func (c Cmd) GetPromptOptSuggests() []prompt.Suggest {
  81. return c.getPromptSuggests(c.optArgs)
  82. }
  83. func (c Cmd) GetPromptPosSuggests() []prompt.Suggest {
  84. return c.getPromptSuggests(c.posArgs)
  85. }
  86. func (c Cmd) GetOptArguments() []structarg.Argument {
  87. return c.getArguments(c.optArgs)
  88. }
  89. func (c Cmd) GetArguments() []structarg.Argument {
  90. ret := c.GetOptArguments()
  91. ret = append(ret, c.GetPosArguments()...)
  92. return ret
  93. }
  94. func (c Cmd) GetPosArguments() []structarg.Argument {
  95. return c.getArguments(c.posArgs)
  96. }
  97. func (c Cmd) getArguments(args []CmdArgument) []structarg.Argument {
  98. ret := make([]structarg.Argument, 0)
  99. for _, a := range args {
  100. ret = append(ret, a.Argument)
  101. }
  102. return ret
  103. }
  104. type CmdArgument struct {
  105. Suggest prompt.Suggest
  106. Argument structarg.Argument
  107. }
  108. func optionCompleter(args []string, long bool) []prompt.Suggest {
  109. l := len(args)
  110. if l == 0 {
  111. return []prompt.Suggest{}
  112. }
  113. if l <= 1 {
  114. if long {
  115. return prompt.FilterHasPrefix(optionHelp, "--", false)
  116. }
  117. return optionHelp
  118. }
  119. if subcmds[args[0]] == nil {
  120. return []prompt.Suggest{}
  121. }
  122. _cmd := subcmds[args[0]].GetPromptOptSuggests()
  123. return prompt.FilterContains(_cmd, strings.TrimLeft(args[l-1], "-"), true)
  124. }
  125. func Completer(d prompt.Document) []prompt.Suggest {
  126. if d.TextBeforeCursor() == "" {
  127. return []prompt.Suggest{}
  128. }
  129. var re = regexp.MustCompile(`(?m)^(?:-d|--debug)\s+`)
  130. s := re.ReplaceAllString(d.TextBeforeCursor(), "")
  131. s = strings.TrimSpace(s)
  132. args := strings.Split(s, " ")
  133. w := d.GetWordBeforeCursor()
  134. // If PIPE is in text before the cursor, returns empty suggestions.
  135. for i := range args {
  136. if args[i] == "|" {
  137. return []prompt.Suggest{}
  138. }
  139. }
  140. // If word before the cursor starts with "-", returns CLI flag options.
  141. if strings.HasPrefix(w, "-") {
  142. return optionCompleter(args, strings.HasPrefix(w, "--"))
  143. }
  144. // Return suggestions for option
  145. if suggests, found := completeOptionArguments(d); found {
  146. return suggests
  147. }
  148. return argumentsCompleter(excludeOptions(args))
  149. }
  150. var commands = []prompt.Suggest{
  151. // Custom command.
  152. {Text: "exit", Description: "Exit this program"},
  153. {Text: "quit", Description: "Exit this program"},
  154. }
  155. func argumentsCompleter(args []string) []prompt.Suggest {
  156. if len(args) == 0 {
  157. return []prompt.Suggest{}
  158. }
  159. if len(args) == 1 {
  160. return prompt.FilterHasPrefix(commands, args[0], true)
  161. }
  162. _cmd, ok := subcmds[args[0]]
  163. if !ok {
  164. return []prompt.Suggest{}
  165. }
  166. if len(args)-1 > len(_cmd.posArgs) {
  167. return []prompt.Suggest{}
  168. }
  169. prm := _cmd.posArgs[len(args)-2]
  170. subcommands := []prompt.Suggest{
  171. prm.Suggest,
  172. }
  173. return prompt.FilterHasPrefix(subcommands, args[len(args)-1], true)
  174. }
  175. func AppendCommand(parentCmd *Cmd, text, desc string) {
  176. commands = append(commands, prompt.Suggest{Text: text, Description: desc})
  177. cmd := &Cmd{
  178. Name: text,
  179. Desc: desc,
  180. posArgs: make([]CmdArgument, 0),
  181. optArgs: make([]CmdArgument, 0),
  182. ParentCmd: parentCmd,
  183. }
  184. subcmds[text] = cmd
  185. parentCmd.AddCmd(cmd)
  186. }
  187. func (c *Cmd) addArgument(target *[]CmdArgument, arg structarg.Argument, argStr string, desc string) {
  188. *target = append(*target, CmdArgument{
  189. Suggest: prompt.Suggest{
  190. Text: argStr,
  191. Description: desc,
  192. },
  193. Argument: arg,
  194. })
  195. }
  196. func (c *Cmd) AddPosArgument(arg structarg.Argument, argStr string, desc string) {
  197. c.addArgument(&c.posArgs, arg, argStr, desc)
  198. }
  199. func (c *Cmd) AddOptArgument(arg structarg.Argument, argStr string, desc string) {
  200. c.addArgument(&c.optArgs, arg, argStr, desc)
  201. }
  202. func AppendPos(text, cmd, desc string, arg structarg.Argument) {
  203. cmdObj := subcmds[text]
  204. cmdObj.AddPosArgument(arg, cmd, desc)
  205. }
  206. func AppendOpt(text, cmd, desc string, arg structarg.Argument) {
  207. cmdObj := subcmds[text]
  208. cmdObj.AddOptArgument(arg, cmd, desc)
  209. }
  210. func GenerateAutoCompleteCmds(rootCmd *Cmd, shell string) string {
  211. var ret = []string{}
  212. var i = 0
  213. if strings.ToLower(shell) == "zsh" {
  214. out := bytes.NewBufferString("")
  215. if err := rootCmd.GenZshCompletion(out); err != nil {
  216. panic(err)
  217. }
  218. return out.String()
  219. }
  220. for _, cmd := range subcmds {
  221. var (
  222. strPosArgs = []string{}
  223. strOptArgs = []string{}
  224. )
  225. for _, posArg := range cmd.GetPromptPosSuggests() {
  226. strPosArgs = append(strPosArgs, posArg.Text)
  227. }
  228. for _, optArg := range cmd.GetPromptOptSuggests() {
  229. strOptArgs = append(strOptArgs, strings.Split(optArg.Text, " ")[0])
  230. }
  231. ret = append(ret, fmt.Sprintf(`arr[%d]="%s# %s %s"`, i, cmd.Name,
  232. strings.Join(strPosArgs, " "), strings.Join(strOptArgs, " ")))
  233. i += 1
  234. }
  235. options := strings.Join(ret, "\n")
  236. if strings.ToLower(shell) == "bash" {
  237. return fmt.Sprintf(BASH_COMPLETE_SCRIPT_1, options, BASH_COMPLETE_SCRIPT_2)
  238. } else {
  239. return ""
  240. }
  241. }