github.com/lmorg/murex@v0.0.0-20240217211045-e081c89cd4ef/shell/autocomplete/flags.go (about) 1 package autocomplete 2 3 import ( 4 "errors" 5 "fmt" 6 "strconv" 7 "strings" 8 "time" 9 10 "github.com/lmorg/murex/debug" 11 "github.com/lmorg/murex/lang" 12 "github.com/lmorg/murex/lang/ref" 13 "github.com/lmorg/murex/utils/json" 14 "github.com/lmorg/murex/utils/lists" 15 "github.com/lmorg/murex/utils/man" 16 "github.com/lmorg/murex/utils/pathsplit" 17 "github.com/lmorg/murex/utils/readline" 18 "github.com/lmorg/murex/utils/which" 19 ) 20 21 // Flags is a struct to store auto-complete options 22 type Flags struct { 23 DynamicPreview string // `f1`` preview 24 IncFiles bool // `true` to include file name completion 25 FileRegexp string // Regexp match for files if IncFiles set 26 IncDirs bool // `true` to include directory navigation completion 27 IncExePath bool // `true` to include binaries in $PATH 28 IncExeAll bool // `true` to include all executable names 29 IncManPage bool // `true` to include man page lookup 30 Flags []string // known supported command line flags for executable 31 FlagsDesc map[string]string // known supported command line flags for executable with descriptions 32 Dynamic string // Use murex script to generate auto-complete suggestions 33 DynamicDesc string // Use murex script to generate auto-complete suggestions with descriptions 34 ListView bool // Display the helps as a "popup menu-like" list rather than grid 35 MapView bool // Like ListView but the description is highlighted instead 36 FlagValues map[string][]Flags // Auto-complete possible values for known flags 37 Optional bool // This nest of flags is optional 38 AllowMultiple bool // Allow multiple flags in this nest 39 AllowNoFlagValue bool // Allow there to be no match 40 Goto string // Jump to another location in the config 41 Alias string // Alias one []Flags to another 42 NestedCommand bool // Jump to another command's flag processing (derived from the previous parameter). eg `sudo command parameters...` 43 ImportCompletion string // Import completion from another command 44 AnyValue bool // deprecated 45 AllowAny bool // Allow any value to be input (eg user input that cannot be pre-determined) 46 AutoBranch bool // Autocomplete trees (eg directory structures) one branch at a time 47 ExecCmdline bool // Execute the commandline and pass it to STDIN when Dynamic/DynamicDesc used (potentially dangerous) 48 CacheTTL int // Length of time in seconds to cache autocomplete (defaults to 0) 49 IgnorePrefix bool // Doesn't filter Dynamic and DynamicDesc results by prefix & allows the prefix to get overwritten in readline 50 //NoFlags bool // `true` to disable Flags[] slice and man page parsing 51 } 52 53 var ( 54 // ExesFlags is map of executables and their supported auto-complete options. 55 ExesFlags = make(map[string][]Flags) 56 57 // ExesFlagsFileRef is a map of which module defined ExesFlags 58 ExesFlagsFileRef = make(map[string]*ref.File) 59 60 // GlobalExes is a pre-populated list of all executables in $PATH. 61 // The point of this is to speed up exe auto-completion. 62 //GlobalExes = make(map[string]bool) 63 GlobalExes = NewGlobalExes() 64 ) 65 66 // UpdateGlobalExeList generates a list of executables in $PATH. This used to be called upon demand but it caused a 67 // slight but highly annoying pause if murex had been sat idle for a while. So now it's an exported function so it can 68 // be run as a background job or upon user request. 69 func UpdateGlobalExeList() { 70 envPath, _ := lang.ShellProcess.Variables.GetString("PATH") 71 72 dirs := which.SplitPath(envPath) 73 74 globalExes := make(map[string]bool) 75 76 for i := range dirs { 77 listExes(dirs[i], globalExes) 78 } 79 80 GlobalExes.Set(&globalExes) 81 } 82 83 // InitExeFlags initializes empty []Flags based on sane defaults and a quick scan of the man pages (OS dependant) 84 func InitExeFlags(exe string) { 85 if len(ExesFlags[exe]) == 0 { 86 flags, descriptions := scanManPages(exe) 87 ExesFlags[exe] = []Flags{{ 88 Flags: flags, 89 FlagsDesc: descriptions, 90 IncFiles: true, 91 AllowMultiple: true, 92 AllowAny: true, 93 }} 94 } 95 } 96 97 type runtimeDumpT struct { 98 FlagValues []Flags 99 FileRef *ref.File 100 } 101 102 // RuntimeDump exports the autocomplete flags and FileRef metadata in a JSON 103 // compatible struct for `runtime` to consume 104 func RuntimeDump() interface{} { 105 dump := make(map[string]runtimeDumpT) 106 107 for exe := range ExesFlags { 108 dump[exe] = runtimeDumpT{ 109 FlagValues: ExesFlags[exe], 110 FileRef: ExesFlagsFileRef[exe], 111 } 112 } 113 114 return dump 115 } 116 117 func scanManPages(exe string) ([]string, map[string]string) { 118 paths := man.GetManPages(exe) 119 return man.ParseByPaths(exe, paths) 120 } 121 122 func allExecutables(includeBuiltins bool) map[string]bool { 123 exes := make(map[string]bool) 124 globalExes := GlobalExes.Get() 125 for k, v := range *globalExes { 126 exes[k] = v 127 } 128 129 if !includeBuiltins { 130 return exes 131 } 132 133 for name := range lang.GoFunctions { 134 exes[name] = true 135 } 136 137 lang.MxFunctions.UpdateMap(exes) 138 lang.GlobalAliases.UpdateMap(exes) 139 140 return exes 141 } 142 143 func match(f *Flags, partial string, args dynamicArgs, act *AutoCompleteT) int { 144 matchPartialFlags(f, partial, act) 145 matchDynamic(f, partial, args, act) 146 147 if f.DynamicPreview != "" { 148 act.PreviewBlock = f.DynamicPreview 149 } 150 151 if f.IncExeAll { 152 pathall := allExecutables(true) 153 act.append(matchExes(partial, pathall)...) 154 155 } else if f.IncExePath { 156 pathexes := allExecutables(false) 157 act.append(matchExes(partial, pathexes)...) 158 } 159 160 if f.IncManPage { 161 flags, descriptions := scanManPages(args.exe) 162 descriptions = lists.CropPartialMapKeys(descriptions, partial) 163 for k, v := range descriptions { 164 act.appendDef(k, v) 165 } 166 act.append(lists.CropPartial(flags, partial)...) 167 } 168 169 switch { 170 case act.CacheDynamic: 171 // do nothing 172 case f.IncFiles: 173 act.append(matchFilesAndDirsWithRegexp(partial, f.FileRegexp, act)...) 174 case f.IncDirs && !f.IncFiles: 175 act.append(matchDirs(partial, act)...) 176 } 177 178 if f.ListView { 179 act.TabDisplayType = readline.TabDisplayList 180 } else if f.MapView { 181 act.TabDisplayType = readline.TabDisplayMap 182 } 183 184 return len(act.Items) 185 } 186 187 func getFlagStructFromPath(flags []Flags, path []string) ([]Flags, int, error) { 188 if len(flags) == 0 { 189 return nil, 0, errors.New("empty []Flags struct found in autocomplete nest") 190 } 191 192 if len(path) == 0 { 193 return flags, 0, nil 194 } 195 196 i, err := strconv.Atoi(path[0]) 197 if err != nil { 198 return nil, 0, fmt.Errorf("unable to convert path index of '%s' into an integer: %s", path[0], err.Error()) 199 } 200 201 if len(path) == 1 { 202 return flags, i, nil 203 } 204 205 if len(flags[i].FlagValues[path[1]]) == 0 { 206 return nil, 0, fmt.Errorf("empty set of flags for value '%s'", path[1]) 207 } 208 209 return getFlagStructFromPath(flags[i].FlagValues[path[1]], path[2:]) 210 } 211 212 var occurrences int 213 214 func matchFlags(flags []Flags, nest int, partial, exe string, params []string, pIndex *int, args dynamicArgs, act *AutoCompleteT) int { 215 occurrences++ 216 if occurrences > 10 { 217 act.ErrCallback(errors.New("autocomplete terminated -- suspected endless goto loop")) 218 return 0 219 } 220 if nest >= len(flags) { 221 act.ErrCallback(fmt.Errorf("nest value of %d is greater than the number of autocomplete instructions (%d)", nest, len(flags))) 222 return 0 223 } 224 225 defer func() { 226 if debug.Enabled { 227 return 228 } 229 if r := recover(); r != nil { 230 lang.ShellProcess.Stderr.Writeln([]byte(fmt.Sprint("\nPanic caught:", r))) 231 lang.ShellProcess.Stderr.Writeln([]byte(fmt.Sprintf("Debug information:\n- partial: '%s'\n- exe: '%s'\n- params: %s\n- pIndex: %d\n- nest: %d\nAutocompletion syntax:", partial, exe, params, *pIndex, nest))) 232 b, _ := json.Marshal(flags, true) 233 lang.ShellProcess.Stderr.Writeln([]byte(string(b))) 234 235 } 236 }() 237 238 if len(flags) > 0 { 239 for ; *pIndex <= len(params); *pIndex++ { 240 next: 241 if time.Now().After(act.TimeOut) { 242 act.ErrCallback(errors.New("autocomplete timed out")) 243 return len(act.Items) 244 } 245 246 if *pIndex >= len(params) { 247 break 248 } 249 250 if *pIndex > 0 && nest > 0 && flags[nest-1].ImportCompletion != "" { 251 act.ParsedTokens.FuncName = flags[nest-1].ImportCompletion 252 act.ParsedTokens.Parameters = []string{partial} 253 MatchFlags(act) 254 return 0 255 } 256 257 if *pIndex > 0 && nest > 0 && flags[nest-1].NestedCommand { 258 //debug.Log("params:", params[*pIndex-1]) 259 InitExeFlags(params[*pIndex-1]) 260 if len(flags[nest-1].FlagValues) == 0 { 261 flags[nest-1].FlagValues = make(map[string][]Flags) 262 } 263 264 // Only nest command if the command isn't present in Flags.Flags[]. Otherwise we then assume that flag 265 // has already been defined by `autocomplete`. 266 // NOTE TO SELF: I can't remember what this does? And is it required for FlagsDesc? 267 var doNotNest bool 268 269 if flags[nest-1].FlagsDesc[params[*pIndex-1]] != "" { 270 doNotNest = true 271 } 272 for i := range flags[nest-1].Flags { 273 if flags[nest-1].Flags[i] == params[*pIndex-1] { 274 doNotNest = true 275 break 276 } 277 } 278 279 if !doNotNest { 280 args.exe = params[*pIndex-1] 281 args.params = params[*pIndex:] 282 args.float = *pIndex 283 flags[nest-1].FlagValues[args.exe] = ExesFlags[args.exe] 284 } 285 } 286 287 if *pIndex > 0 && nest > 0 { 288 var length int 289 290 if len(flags[nest-1].FlagValues[params[*pIndex-1]]) > 0 { 291 alias := flags[nest-1].FlagValues[params[*pIndex-1]][0].Alias 292 if alias != "" { 293 flags[nest-1].FlagValues[params[*pIndex-1]] = flags[nest-1].FlagValues[alias] 294 } 295 296 length = matchFlags(flags[nest-1].FlagValues[params[*pIndex-1]], 0, partial, exe, params, pIndex, args, act) 297 } 298 299 if len(flags[nest-1].FlagValues["*"]) > 0 && (len(flags[nest-1].FlagValues[params[*pIndex-1]]) > 0 || 300 flags[nest-1].FlagsDesc[params[*pIndex-1]] != "" || 301 lists.Match(flags[nest-1].Flags, params[*pIndex-1])) { 302 303 alias := flags[nest-1].FlagValues["*"][0].Alias 304 if alias != "" { 305 flags[nest-1].FlagValues["*"] = flags[nest-1].FlagValues[alias] 306 } 307 308 length += matchFlags(flags[nest-1].FlagValues["*"], 0, partial, exe, params, pIndex, args, act) 309 } 310 311 if len(flags[nest-1].FlagValues[""]) > 0 { 312 alias := flags[nest-1].FlagValues[""][0].Alias 313 if alias != "" { 314 flags[nest-1].FlagValues[""] = flags[nest-1].FlagValues[alias] 315 } 316 317 length += matchFlags(flags[nest-1].FlagValues[""], 0, partial, exe, params, pIndex, args, act) 318 } 319 320 if length > 0 && !flags[nest-1].AllowNoFlagValue { 321 return len(act.Items) 322 } 323 } 324 325 if nest >= len(flags) { 326 return len(act.Items) 327 } 328 329 if flags[nest].Goto != "" { 330 split, err := pathsplit.Split(flags[nest].Goto) 331 if err != nil { 332 act.ErrCallback(err) 333 return 0 334 } 335 336 f, i, err := getFlagStructFromPath(ExesFlags[exe], split) 337 if err != nil { 338 act.ErrCallback(err) 339 return 0 340 } 341 342 return matchFlags(f, i, partial, exe, params, pIndex, args, act) 343 } 344 345 if nest >= len(flags) || *pIndex >= len(params) { 346 break 347 } 348 length := match(&flags[nest], params[*pIndex], dynamicArgs{exe: args.exe, params: params[args.float:*pIndex]}, act.disposable()) 349 if flags[nest].AllowAny || flags[nest].AnyValue || length > 0 { 350 if !flags[nest].AllowMultiple { 351 nest++ 352 } 353 continue 354 } 355 356 nest++ 357 goto next 358 } 359 } 360 361 if nest > 0 { 362 nest-- 363 } 364 365 for ; nest <= len(flags); nest++ { 366 if nest >= len(flags) { 367 /* I don't know why this is needed but it catches a segfault with the following code: 368 369 autocomplete set docgen { [ 370 { 371 "AllowMultiple": true, 372 "Optional": true, 373 "FlagsDesc": { 374 "-panic": "Write a stack trace on error", 375 "-readonly": "Don't write output to disk. Use this to test the config", 376 "-verbose": "Verbose output (all log messages inc warnings)", 377 "-version": "Output docgen version number and exit", 378 "-warning": "Display warning messages (will also return a non-zero exit status if warnings found)", 379 "-config": "Location of the base docgen config file" 380 }, 381 "FlagValues": { 382 "-config": [{ 383 "IncFiles": true 384 }] 385 } 386 } 387 ] } */ 388 break 389 } 390 391 match(&flags[nest], partial, args, act) 392 if !flags[nest].Optional { 393 break 394 } 395 } 396 397 return len(act.Items) 398 } 399 400 func matchPartialFlags(f *Flags, partial string, act *AutoCompleteT) { 401 var flag string 402 403 for i := range f.Flags { 404 flag = f.Flags[i] 405 if flag == "" { 406 continue 407 } 408 if strings.HasPrefix(flag, partial) { 409 act.append(flag[len(partial):]) 410 } 411 } 412 413 for flag := range f.FlagsDesc { 414 if !strings.HasPrefix(flag, partial) { 415 continue 416 } 417 418 act.appendDef(flag[len(partial):], f.FlagsDesc[flag]) 419 } 420 }