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  }