github.com/pachyderm/pachyderm@v1.13.4/src/server/cmd/pachctl/shell/shell.go (about) 1 package shell 2 3 import ( 4 "fmt" 5 "log" 6 "os" 7 "os/exec" 8 "strings" 9 10 prompt "github.com/c-bata/go-prompt" 11 "github.com/fatih/color" 12 "github.com/pachyderm/pachyderm/src/client/pkg/config" 13 "github.com/pachyderm/pachyderm/src/server/pkg/uuid" 14 "github.com/spf13/cobra" 15 ) 16 17 const ( 18 completionAnnotation string = "completion" 19 ldThreshold int = 2 20 defaultMaxCompletions int64 = 64 21 ) 22 23 // CacheFunc is a function which returns whether or not cached results from a 24 // previous call to a CompletionFunc can be reused. 25 type CacheFunc func(flag, text string) bool 26 27 // CacheAll is a CacheFunc that always returns true (always use cached results). 28 func CacheAll(_, _ string) bool { return true } 29 30 // CacheNone is a CacheFunc that always returns false (never cache anything). 31 func CacheNone(_, _ string) bool { return false } 32 33 // SameFlag is a CacheFunc that returns true if the flags are the same. 34 func SameFlag(flag string) CacheFunc { 35 return func(_flag, _ string) bool { 36 return flag == _flag 37 } 38 } 39 40 // AndCacheFunc ands 0 or more cache funcs together. 41 func AndCacheFunc(fs ...CacheFunc) CacheFunc { 42 return func(flag, text string) bool { 43 for _, f := range fs { 44 if !f(flag, text) { 45 return false 46 } 47 } 48 return true 49 } 50 } 51 52 // CompletionFunc is a function which returns completions for a command. 53 type CompletionFunc func(flag, text string, maxCompletions int64) ([]prompt.Suggest, CacheFunc) 54 55 var completions map[string]CompletionFunc = make(map[string]CompletionFunc) 56 57 // RegisterCompletionFunc registers a completion function for a command. 58 // NOTE: RegisterCompletionFunc must be called before cmd is passed to 59 // functions that make copies of it (such as cmdutil.CreateAlias. This is 60 // because RegisterCompletionFunc modifies cmd in a superficial way by adding 61 // an annotation (to the Annotations field) that associates it with the 62 // completion function. This means that 63 func RegisterCompletionFunc(cmd *cobra.Command, completionFunc CompletionFunc) { 64 id := uuid.NewWithoutDashes() 65 66 if cmd.Annotations == nil { 67 cmd.Annotations = make(map[string]string) 68 } else if _, ok := cmd.Annotations[completionAnnotation]; ok { 69 panic("duplicate completion func registration") 70 } 71 cmd.Annotations[completionAnnotation] = id 72 completions[id] = completionFunc 73 } 74 75 type shell struct { 76 rootCmd *cobra.Command 77 maxCompletions int64 78 79 // variables for caching completion calls 80 completionID string 81 suggests []prompt.Suggest 82 cacheF CacheFunc 83 } 84 85 func newShell(rootCmd *cobra.Command, maxCompletions int64) *shell { 86 if maxCompletions == 0 { 87 maxCompletions = defaultMaxCompletions 88 } 89 return &shell{ 90 rootCmd: rootCmd, 91 maxCompletions: maxCompletions, 92 } 93 } 94 95 func (s *shell) executor(in string) { 96 if in == "exit" { 97 os.Exit(0) 98 } 99 100 cmd := exec.Command("bash") 101 cmd.Stdin = strings.NewReader("pachctl " + in) 102 cmd.Stdout = os.Stdout 103 cmd.Stderr = os.Stderr 104 cmd.Run() 105 } 106 107 func (s *shell) suggestor(in prompt.Document) []prompt.Suggest { 108 args := strings.Fields(in.Text) 109 if len(strings.TrimSuffix(in.Text, " ")) < len(in.Text) { 110 args = append(args, "") 111 } 112 cmd := s.rootCmd 113 text := "" 114 if len(args) > 0 { 115 var err error 116 cmd, _, err = s.rootCmd.Traverse(args[:len(args)-1]) 117 if err != nil { 118 log.Fatal(err) 119 } 120 text = args[len(args)-1] 121 } 122 flag := "" 123 if len(args) > 1 { 124 if args[len(args)-2][0] == '-' { 125 flag = args[len(args)-2] 126 } 127 } 128 suggestions := cmd.SuggestionsFor(text) 129 if len(suggestions) > 0 { 130 var result []prompt.Suggest 131 for _, suggestion := range suggestions { 132 cmd, _, err := cmd.Traverse([]string{suggestion}) 133 if err != nil { 134 log.Fatal(err) 135 } 136 result = append(result, prompt.Suggest{ 137 Text: suggestion, 138 Description: cmd.Short, 139 }) 140 } 141 return result 142 } 143 if id, ok := cmd.Annotations[completionAnnotation]; ok { 144 completionFunc := completions[id] 145 if s.completionID != id || s.cacheF == nil || !s.cacheF(flag, text) { 146 s.completionID = id 147 s.suggests, s.cacheF = completionFunc(flag, text, s.maxCompletions) 148 } 149 var result []prompt.Suggest 150 for _, sug := range s.suggests { 151 sText := sug.Text 152 if len(text) < len(sText) { 153 sText = sText[:len(text)] 154 } 155 if ld(sText, text, true) < ldThreshold { 156 result = append(result, sug) 157 } 158 if int64(len(result)) > s.maxCompletions { 159 break 160 } 161 } 162 return result 163 } 164 return nil 165 } 166 167 func (s *shell) clearCache() { 168 s.completionID = "" 169 s.suggests = nil 170 s.cacheF = nil 171 } 172 173 func (s *shell) run() { 174 fmt.Printf("Type 'exit' or press Ctrl-D to exit.\n") 175 color.NoColor = true // color doesn't work in terminal 176 prompt.New( 177 s.executor, 178 s.suggestor, 179 prompt.OptionPrefix(">>> "), 180 prompt.OptionTitle("Pachyderm Shell"), 181 prompt.OptionAddKeyBind(prompt.KeyBind{ 182 Key: prompt.F5, 183 Fn: func(*prompt.Buffer) { s.clearCache() }, 184 }), 185 prompt.OptionLivePrefix(func() (string, bool) { 186 cfg, err := config.Read(true, false) 187 if err != nil { 188 return "", false 189 } 190 activeContext, _, err := cfg.ActiveContext(false) 191 if err != nil { 192 return "", false 193 } 194 return fmt.Sprintf("context:(%s) >>> ", activeContext), true 195 }), 196 ).Run() 197 if err := closePachClient(); err != nil { 198 log.Fatal(err) 199 } 200 } 201 202 // Run runs a prompt, it does not return. 203 func Run(rootCmd *cobra.Command, maxCompletions int64) { 204 newShell(rootCmd, maxCompletions).run() 205 } 206 207 // ld computes the Levenshtein Distance for two strings. 208 func ld(s, t string, ignoreCase bool) int { 209 if ignoreCase { 210 s = strings.ToLower(s) 211 t = strings.ToLower(t) 212 } 213 d := make([][]int, len(s)+1) 214 for i := range d { 215 d[i] = make([]int, len(t)+1) 216 } 217 for i := range d { 218 d[i][0] = i 219 } 220 for j := range d[0] { 221 d[0][j] = j 222 } 223 for j := 1; j <= len(t); j++ { 224 for i := 1; i <= len(s); i++ { 225 if s[i-1] == t[j-1] { 226 d[i][j] = d[i-1][j-1] 227 } else { 228 min := d[i-1][j] 229 if d[i][j-1] < min { 230 min = d[i][j-1] 231 } 232 if d[i-1][j-1] < min { 233 min = d[i-1][j-1] 234 } 235 d[i][j] = min + 1 236 } 237 } 238 239 } 240 return d[len(s)][len(t)] 241 }