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 }