github.com/lmorg/murex@v0.0.0-20240217211045-e081c89cd4ef/shell/history/vars.go (about)

     1  package history
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"github.com/lmorg/murex/lang"
    10  	"github.com/lmorg/murex/utils/readline"
    11  )
    12  
    13  var (
    14  	rxHistIndex   = regexp.MustCompile(`(\^[0-9]+)`)
    15  	rxHistRegex   = regexp.MustCompile(`\^m/(.*?[^\\])/`) // Scratchpad: https://play.golang.org/p/Iya2Hx1uxb
    16  	rxHistPrefix  = regexp.MustCompile(`(\^\^[a-zA-Z]+)`)
    17  	rxHistTag     = regexp.MustCompile(`(\^#[-_a-zA-Z0-9]+)`)
    18  	rxHistParam   = regexp.MustCompile(`\^\[([-]?[0-9]+)]`)
    19  	rxHistReplace = regexp.MustCompile(`\^s/(.*?[^\\])/(.*?[^\\])/`)
    20  )
    21  
    22  const (
    23  	errCannotParsePrevCmd = "cannot parse previous command line to extract parameters for history variable"
    24  )
    25  
    26  func getLine(i int, rl *readline.Instance) (s string) {
    27  	s, _ = rl.History.GetLine(i)
    28  	return
    29  }
    30  
    31  // ExpandVariables finds all history variables and replaces them with the value
    32  // of the variable
    33  func ExpandVariables(line []rune, rl *readline.Instance) ([]rune, error) {
    34  	return expandVariables(line, rl, false)
    35  }
    36  
    37  // ExpandVariablesInLine finds history variables in a line and replaces it with
    38  // the value of the variable. It does not replace the line formatting variables.
    39  func ExpandVariablesInLine(line []rune, rl *readline.Instance) ([]rune, error) {
    40  	return expandVariables(line, rl, true)
    41  }
    42  
    43  func expandVariables(line []rune, rl *readline.Instance, skipFormatting bool) ([]rune, error) {
    44  	s := string(line)
    45  
    46  	escape := string([]byte{0, 1, 2, 2})
    47  	s = strings.ReplaceAll(s, `\^`, escape)
    48  
    49  	if !skipFormatting {
    50  		s = strings.Replace(s, `^\n`, "\n", -1) // Match new line
    51  		s = strings.Replace(s, `^\t`, "\t", -1) // Match tab
    52  	}
    53  
    54  	funcs := []func(string, *readline.Instance) (string, error){
    55  		expandHistBangBang,
    56  		expandHistPrefix,
    57  		expandHistIndex,
    58  		expandHistRegex,
    59  		expandHistHashtag,
    60  		expandHistParam,
    61  		expandHistReplace,
    62  	}
    63  
    64  	for f := range funcs {
    65  		var err error
    66  		s, err = funcs[f](s, rl)
    67  		if err != nil {
    68  			return nil, err
    69  		}
    70  	}
    71  
    72  	s = strings.ReplaceAll(s, escape, `\^`)
    73  	return []rune(s), nil
    74  }
    75  
    76  // Match last command
    77  func expandHistBangBang(s string, rl *readline.Instance) (string, error) {
    78  	last := getLine(rl.History.Len()-1, rl)
    79  	return strings.Replace(s, "^!!", noColon(last), -1), nil
    80  }
    81  
    82  // Match history index
    83  func expandHistIndex(s string, rl *readline.Instance) (string, error) {
    84  	mhIndex := rxHistIndex.FindAllString(s, -1)
    85  	for i := range mhIndex {
    86  		val, _ := strconv.Atoi(mhIndex[i][1:])
    87  		if val > rl.History.Len() {
    88  			return "", fmt.Errorf("(%s) Value greater than history length in ^%d", mhIndex[i], val)
    89  		}
    90  		s = rxHistIndex.ReplaceAllString(s, noColon(getLine(val, rl)))
    91  		//return s, nil
    92  	}
    93  
    94  	return s, nil
    95  }
    96  
    97  // Match history by regexp
    98  func expandHistRegex(s string, rl *readline.Instance) (string, error) {
    99  	mhRegexp := rxHistRegex.FindAllStringSubmatch(s, -1)
   100  	for i := range mhRegexp {
   101  		rx, err := regexp.Compile(mhRegexp[i][1])
   102  		if err != nil {
   103  			return "", fmt.Errorf("(%s) Regexp error in history variable `^m/%s/`: %s", mhRegexp[i][0], mhRegexp[i][1], err.Error())
   104  		}
   105  
   106  		for h := rl.History.Len() - 1; h > -1; h-- {
   107  			if rx.MatchString(getLine(h, rl)) {
   108  				s = rxHistRegex.ReplaceAllString(s, noColon(getLine(h, rl)))
   109  				goto next
   110  			}
   111  		}
   112  		return "", fmt.Errorf("(%s) Cannot find a history item to match regexp: %s", mhRegexp[i][0], mhRegexp[i][1])
   113  	next:
   114  	}
   115  	return s, nil
   116  }
   117  
   118  // Match history hashtag
   119  func expandHistHashtag(s string, rl *readline.Instance) (string, error) {
   120  	mhTag := rxHistTag.FindAllString(s, -1)
   121  
   122  	for i := range mhTag {
   123  		for h := rl.History.Len() - 1; h > -1; h-- {
   124  			line := getLine(h, rl)
   125  			if strings.HasSuffix(line, mhTag[i][1:]) {
   126  				block := line[:len(line)-len(mhTag[i][1:])]
   127  				s = strings.Replace(s, mhTag[i], noColon(block), 1)
   128  
   129  				goto next
   130  			}
   131  		}
   132  
   133  		return "", fmt.Errorf("(%s) Hashtag not found", mhTag[i])
   134  	next:
   135  	}
   136  
   137  	return s, nil
   138  }
   139  
   140  // Match last params (first command in block)
   141  func expandHistParam(s string, rl *readline.Instance) (string, error) {
   142  	mhParam := rxHistParam.FindAllStringSubmatch(s, -1)
   143  	if len(mhParam) > 0 {
   144  		last := getLine(rl.History.Len()-1, rl)
   145  		nodes, err := lang.ParseBlock([]rune(last))
   146  		if err != nil || len(*nodes) == 0 {
   147  			return "", fmt.Errorf(errCannotParsePrevCmd)
   148  		}
   149  
   150  		cmd := &(*nodes)[0]
   151  		l := len(cmd.Parameters)
   152  		for i := range mhParam {
   153  			val, _ := strconv.Atoi(mhParam[i][1])
   154  			if val < 0 {
   155  				val += l + 1
   156  			}
   157  
   158  			switch {
   159  			case val == 0:
   160  				s = strings.Replace(s, mhParam[i][0], string(cmd.Command), -1)
   161  			case val > 0 && val-1 < l:
   162  				s = strings.Replace(s, mhParam[i][0], string(cmd.Parameters[val-1]), -1)
   163  			default:
   164  				s = strings.Replace(s, mhParam[i][0], "", -1)
   165  				return s, fmt.Errorf("(%s) No parameter with index %s", mhParam[i][0], mhParam[i][1])
   166  			}
   167  
   168  		}
   169  
   170  	}
   171  	return s, nil
   172  }
   173  
   174  // Replace string from command buffer
   175  func expandHistReplace(s string, rl *readline.Instance) (string, error) {
   176  	sList := rxHistReplace.FindAllStringSubmatch(s, -1)
   177  	var rxList []*regexp.Regexp
   178  	var replaceList []string
   179  
   180  	for i := range sList {
   181  		rx, err := regexp.Compile(sList[i][1])
   182  		if err != nil || len(sList[i]) != 3 {
   183  			return "", fmt.Errorf("(%s) Regexp error in history variable `^s/%s/%s`: %s", sList[i][0], sList[i][1], sList[i][2], err.Error())
   184  		}
   185  		rxList = append(rxList, rx)
   186  		replaceList = append(replaceList, sList[i][2])
   187  		s = strings.Replace(s, sList[i][0], "", -1)
   188  	}
   189  	for i := range rxList {
   190  		s = rxList[i].ReplaceAllString(s, replaceList[i])
   191  	}
   192  
   193  	return s, nil
   194  }
   195  
   196  // Match history prefix
   197  func expandHistPrefix(s string, rl *readline.Instance) (string, error) {
   198  	mhPrefix := rxHistPrefix.FindAllString(s, -1)
   199  	for i := range mhPrefix {
   200  		for h := rl.History.Len() - 1; h > -1; h-- {
   201  			if strings.HasPrefix(getLine(h, rl), mhPrefix[i][2:]) {
   202  				s = strings.Replace(s, mhPrefix[i], noColon(getLine(h, rl)), 1)
   203  
   204  				goto next
   205  			}
   206  		}
   207  
   208  		return "", fmt.Errorf("(%s) Cannot find a history item to match prefix", mhPrefix[i])
   209  	next:
   210  	}
   211  
   212  	return s, nil
   213  }