src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/eval/glob.go (about) 1 package eval 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "strings" 9 "unicode" 10 11 "src.elv.sh/pkg/eval/vals" 12 "src.elv.sh/pkg/glob" 13 "src.elv.sh/pkg/parse" 14 ) 15 16 // An ephemeral value generated when evaluating tilde and wildcards. 17 type globPattern struct { 18 glob.Pattern 19 Flags globFlag 20 Buts []string 21 TypeCb func(os.FileMode) bool 22 } 23 24 type globFlag uint 25 26 var typeCbMap = map[string]func(os.FileMode) bool{ 27 "dir": os.FileMode.IsDir, 28 "regular": os.FileMode.IsRegular, 29 } 30 31 const ( 32 // noMatchOK indicates that the "nomatch-ok" glob index modifier was 33 // present. 34 noMatchOK globFlag = 1 << iota 35 ) 36 37 func (f globFlag) Has(g globFlag) bool { 38 return (f & g) == g 39 } 40 41 var _ vals.ErrIndexer = globPattern{} 42 43 var ( 44 ErrMustFollowWildcard = errors.New("must follow wildcard") 45 ErrModifierMustBeString = errors.New("modifier must be string") 46 ErrWildcardNoMatch = errors.New("wildcard has no match") 47 ErrMultipleTypeModifiers = errors.New("only one type modifier allowed") 48 ErrUnknownTypeModifier = errors.New("unknown type modifier") 49 ) 50 51 var runeMatchers = map[string]func(rune) bool{ 52 "control": unicode.IsControl, 53 "digit": unicode.IsDigit, 54 "graphic": unicode.IsGraphic, 55 "letter": unicode.IsLetter, 56 "lower": unicode.IsDigit, 57 "mark": unicode.IsMark, 58 "number": unicode.IsNumber, 59 "print": unicode.IsPrint, 60 "punct": unicode.IsPunct, 61 "space": unicode.IsSpace, 62 "symbol": unicode.IsSymbol, 63 "title": unicode.IsTitle, 64 "upper": unicode.IsUpper, 65 } 66 67 func (gp globPattern) Kind() string { return "glob-pattern" } 68 69 func (gp globPattern) Index(k any) (any, error) { 70 modifierv, ok := k.(string) 71 if !ok { 72 return nil, ErrModifierMustBeString 73 } 74 modifier := modifierv 75 switch { 76 case modifier == "nomatch-ok": 77 gp.Flags |= noMatchOK 78 case strings.HasPrefix(modifier, "but:"): 79 gp.Buts = append(gp.Buts, modifier[len("but:"):]) 80 case modifier == "match-hidden": 81 lastSeg, err := gp.lastWildSeg() 82 if err != nil { 83 return nil, err 84 } 85 gp.Segments[len(gp.Segments)-1] = glob.Wild{ 86 Type: lastSeg.Type, MatchHidden: true, Matchers: lastSeg.Matchers, 87 } 88 case strings.HasPrefix(modifier, "type:"): 89 if gp.TypeCb != nil { 90 return nil, ErrMultipleTypeModifiers 91 } 92 typeName := modifier[len("type:"):] 93 cb, ok := typeCbMap[typeName] 94 if !ok { 95 return nil, ErrUnknownTypeModifier 96 } 97 gp.TypeCb = cb 98 default: 99 var matcher func(rune) bool 100 if m, ok := runeMatchers[modifier]; ok { 101 matcher = m 102 } else if strings.HasPrefix(modifier, "set:") { 103 set := modifier[len("set:"):] 104 matcher = func(r rune) bool { 105 return strings.ContainsRune(set, r) 106 } 107 } else if strings.HasPrefix(modifier, "range:") { 108 rangeExpr := modifier[len("range:"):] 109 badRangeExpr := fmt.Errorf("bad range modifier: %s", parse.Quote(rangeExpr)) 110 runes := []rune(rangeExpr) 111 if len(runes) != 3 { 112 return nil, badRangeExpr 113 } 114 from, sep, to := runes[0], runes[1], runes[2] 115 switch sep { 116 case '-': 117 matcher = func(r rune) bool { 118 return from <= r && r <= to 119 } 120 case '~': 121 matcher = func(r rune) bool { 122 return from <= r && r < to 123 } 124 default: 125 return nil, badRangeExpr 126 } 127 } else { 128 return nil, fmt.Errorf("unknown modifier %s", vals.ReprPlain(modifierv)) 129 } 130 err := gp.addMatcher(matcher) 131 return gp, err 132 } 133 return gp, nil 134 } 135 136 func (gp globPattern) Concat(v any) (any, error) { 137 switch rhs := v.(type) { 138 case string: 139 var segs []glob.Segment 140 segs = append(segs, gp.Segments...) 141 segs = append(segs, stringToSegments(rhs)...) 142 return globPattern{Pattern: glob.Pattern{Segments: segs}, Flags: gp.Flags, 143 Buts: gp.Buts, TypeCb: gp.TypeCb}, nil 144 case globPattern: 145 // We know rhs contains exactly one segment. 146 gp.append(rhs.Segments[0]) 147 gp.Flags |= rhs.Flags 148 gp.Buts = append(gp.Buts, rhs.Buts...) 149 // This handles illegal cases such as `**[type:regular]x*[type:directory]`. 150 if gp.TypeCb != nil && rhs.TypeCb != nil { 151 return nil, ErrMultipleTypeModifiers 152 } 153 if rhs.TypeCb != nil { 154 gp.TypeCb = rhs.TypeCb 155 } 156 return gp, nil 157 } 158 159 return nil, vals.ErrConcatNotImplemented 160 } 161 162 func (gp globPattern) RConcat(v any) (any, error) { 163 switch lhs := v.(type) { 164 case string: 165 segs := stringToSegments(lhs) 166 // We know gp contains exactly one segment. 167 segs = append(segs, gp.Segments[0]) 168 return globPattern{Pattern: glob.Pattern{Segments: segs}, Flags: gp.Flags, 169 Buts: gp.Buts, TypeCb: gp.TypeCb}, nil 170 } 171 172 return nil, vals.ErrConcatNotImplemented 173 } 174 175 func (gp *globPattern) lastWildSeg() (glob.Wild, error) { 176 if len(gp.Segments) == 0 { 177 return glob.Wild{}, ErrBadglobPattern 178 } 179 if !glob.IsWild(gp.Segments[len(gp.Segments)-1]) { 180 return glob.Wild{}, ErrMustFollowWildcard 181 } 182 return gp.Segments[len(gp.Segments)-1].(glob.Wild), nil 183 } 184 185 func (gp *globPattern) addMatcher(matcher func(rune) bool) error { 186 lastSeg, err := gp.lastWildSeg() 187 if err != nil { 188 return err 189 } 190 gp.Segments[len(gp.Segments)-1] = glob.Wild{ 191 Type: lastSeg.Type, MatchHidden: lastSeg.MatchHidden, 192 Matchers: append(lastSeg.Matchers, matcher), 193 } 194 return nil 195 } 196 197 func (gp *globPattern) append(segs ...glob.Segment) { 198 gp.Segments = append(gp.Segments, segs...) 199 } 200 201 func wildcardToSegment(s string) (glob.Segment, error) { 202 switch s { 203 case "*": 204 return glob.Wild{Type: glob.Star, MatchHidden: false, Matchers: nil}, nil 205 case "**": 206 return glob.Wild{Type: glob.StarStar, MatchHidden: false, Matchers: nil}, nil 207 case "?": 208 return glob.Wild{Type: glob.Question, MatchHidden: false, Matchers: nil}, nil 209 default: 210 return nil, fmt.Errorf("bad wildcard: %q", s) 211 } 212 } 213 214 func stringToSegments(s string) []glob.Segment { 215 segs := []glob.Segment{} 216 for i := 0; i < len(s); { 217 j := i 218 for ; j < len(s) && s[j] != '/'; j++ { 219 } 220 if j > i { 221 segs = append(segs, glob.Literal{Data: s[i:j]}) 222 } 223 if j < len(s) { 224 for ; j < len(s) && s[j] == '/'; j++ { 225 } 226 segs = append(segs, glob.Slash{}) 227 i = j 228 } else { 229 break 230 } 231 } 232 return segs 233 } 234 235 func doGlob(ctx context.Context, gp globPattern) ([]any, error) { 236 but := make(map[string]struct{}) 237 for _, s := range gp.Buts { 238 but[s] = struct{}{} 239 } 240 241 vs := make([]any, 0) 242 if !gp.Glob(func(pathInfo glob.PathInfo) bool { 243 select { 244 case <-ctx.Done(): 245 logger.Println("glob aborted") 246 return false 247 default: 248 } 249 250 if _, ignore := but[pathInfo.Path]; ignore { 251 return true 252 } 253 254 if gp.TypeCb == nil || gp.TypeCb(pathInfo.Info.Mode()) { 255 vs = append(vs, pathInfo.Path) 256 } 257 return true 258 }) { 259 return nil, ErrInterrupted 260 } 261 if len(vs) == 0 && !gp.Flags.Has(noMatchOK) { 262 return nil, ErrWildcardNoMatch 263 } 264 return vs, nil 265 }