github.com/bingoohuang/gg@v0.0.0-20240325092523-45da7dee9335/pkg/fn/match.go (about) 1 // Copyright 2010 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package fn 6 7 import ( 8 "errors" 9 "os" 10 "runtime" 11 "strings" 12 "unicode/utf8" 13 ) 14 15 const Separator = os.PathSeparator 16 17 // ErrBadPattern indicates a pattern was malformed. 18 var ErrBadPattern = errors.New("syntax error in pattern") 19 20 type MatchOptions struct { 21 CaseInsensitive bool 22 } 23 24 type ( 25 MatchOptionsFn func(*MatchOptions) 26 MatchOptionsFns []MatchOptionsFn 27 ) 28 29 func WithCaseSensitive(v bool) MatchOptionsFn { 30 return func(o *MatchOptions) { o.CaseInsensitive = !v } 31 } 32 33 func (f MatchOptionsFns) CreateOptions() (options MatchOptions) { 34 for _, fn := range f { 35 fn(&options) 36 } 37 38 return 39 } 40 41 func Match(pattern, name string, matchOptions ...MatchOptionsFn) (matched bool) { 42 matched, _ = MatchE(pattern, name, matchOptions...) 43 return 44 } 45 46 // Match reports whether name matches the shell file name pattern in case insensitive for non-pattern parts. 47 // The pattern syntax is: 48 // 49 // pattern: 50 // { term } 51 // term: 52 // '*' matches any sequence of non-Separator characters 53 // '?' matches any single non-Separator character 54 // '[' [ '^' ] { character-range } ']' 55 // character class (must be non-empty) 56 // c matches character c (c != '*', '?', '\\', '[') 57 // '\\' c matches character c 58 // 59 // character-range: 60 // c matches character c (c != '\\', '-', ']') 61 // '\\' c matches character c 62 // lo '-' hi matches character c for lo <= c <= hi 63 // 64 // Match requires pattern to match all of name, not just a substring. 65 // The only possible returned error is ErrBadPattern, when pattern 66 // is malformed. 67 // 68 // On Windows, escaping is disabled. Instead, '\\' is treated as 69 // path separator. 70 func MatchE(pattern, name string, matchOptions ...MatchOptionsFn) (matched bool, err error) { 71 options := MatchOptionsFns(matchOptions).CreateOptions() 72 Pattern: 73 for len(pattern) > 0 { 74 var star bool 75 var chunk string 76 star, chunk, pattern = scanChunk(pattern) 77 if star && chunk == "" { 78 // Trailing * matches rest of string unless it has a /. 79 return !strings.Contains(name, string(Separator)), nil 80 } 81 // Look for match at current position. 82 t, ok, err := matchChunk(chunk, name, options) 83 // if we're the last chunk, make sure we've exhausted the name 84 // otherwise we'll give a false result even if we could still match 85 // using the star 86 if ok && (len(t) == 0 || len(pattern) > 0) { 87 name = t 88 continue 89 } 90 if err != nil { 91 return false, err 92 } 93 if star { 94 // Look for match skipping i+1 bytes. 95 // Cannot skip /. 96 for i := 0; i < len(name) && name[i] != Separator; i++ { 97 t, ok, err := matchChunk(chunk, name[i+1:], options) 98 if ok { 99 // if we're the last chunk, make sure we exhausted the name 100 if len(pattern) == 0 && len(t) > 0 { 101 continue 102 } 103 name = t 104 continue Pattern 105 } 106 if err != nil { 107 return false, err 108 } 109 } 110 } 111 return false, nil 112 } 113 return len(name) == 0, nil 114 } 115 116 // scanChunk gets the next segment of pattern, which is a non-star string 117 // possibly preceded by a star. 118 func scanChunk(pattern string) (star bool, chunk, rest string) { 119 for len(pattern) > 0 && pattern[0] == '*' { 120 pattern = pattern[1:] 121 star = true 122 } 123 inrange := false 124 var i int 125 Scan: 126 for i = 0; i < len(pattern); i++ { 127 switch pattern[i] { 128 case '\\': 129 if runtime.GOOS != "windows" { 130 // error check handled in matchChunk: bad pattern. 131 if i+1 < len(pattern) { 132 i++ 133 } 134 } 135 case '[': 136 inrange = true 137 case ']': 138 inrange = false 139 case '*': 140 if !inrange { 141 break Scan 142 } 143 } 144 } 145 return star, pattern[0:i], pattern[i:] 146 } 147 148 // matchChunk checks whether chunk matches the beginning of s. 149 // If so, it returns the remainder of s (after the match). 150 // Chunk is all single-character operators: literals, char classes, and ?. 151 func matchChunk(chunk, s string, options MatchOptions) (rest string, ok bool, err error) { 152 // failed records whether the match has failed. 153 // After the match fails, the loop continues on processing chunk, 154 // checking that the pattern is well-formed but no longer reading s. 155 failed := false 156 for len(chunk) > 0 { 157 if !failed && len(s) == 0 { 158 failed = true 159 } 160 switch chunk[0] { 161 case '[': 162 // character class 163 var r rune 164 if !failed { 165 var n int 166 r, n = utf8.DecodeRuneInString(s) 167 s = s[n:] 168 } 169 chunk = chunk[1:] 170 // possibly negated 171 negated := false 172 if len(chunk) > 0 && chunk[0] == '^' { 173 negated = true 174 chunk = chunk[1:] 175 } 176 // parse all ranges 177 match := false 178 nrange := 0 179 for { 180 if len(chunk) > 0 && chunk[0] == ']' && nrange > 0 { 181 chunk = chunk[1:] 182 break 183 } 184 var lo, hi rune 185 if lo, chunk, err = getEsc(chunk); err != nil { 186 return "", false, err 187 } 188 hi = lo 189 if chunk[0] == '-' { 190 if hi, chunk, err = getEsc(chunk[1:]); err != nil { 191 return "", false, err 192 } 193 } 194 if lo <= r && r <= hi { 195 match = true 196 } 197 nrange++ 198 } 199 if match == negated { 200 failed = true 201 } 202 203 case '?': 204 if !failed { 205 if s[0] == Separator { 206 failed = true 207 } 208 _, n := utf8.DecodeRuneInString(s) 209 s = s[n:] 210 } 211 chunk = chunk[1:] 212 213 case '\\': 214 if runtime.GOOS != "windows" { 215 chunk = chunk[1:] 216 if len(chunk) == 0 { 217 return "", false, ErrBadPattern 218 } 219 } 220 fallthrough 221 222 default: 223 if !failed { 224 if options.CaseInsensitive { 225 if !strings.EqualFold(chunk[:1], s[:1]) { 226 failed = true 227 } 228 } else { 229 if chunk[0] != s[0] { 230 failed = true 231 } 232 } 233 s = s[1:] 234 } 235 chunk = chunk[1:] 236 } 237 } 238 if failed { 239 return "", false, nil 240 } 241 return s, true, nil 242 } 243 244 // getEsc gets a possibly-escaped character from chunk, for a character class. 245 func getEsc(chunk string) (r rune, nchunk string, err error) { 246 if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' { 247 err = ErrBadPattern 248 return 249 } 250 if chunk[0] == '\\' && runtime.GOOS != "windows" { 251 chunk = chunk[1:] 252 if len(chunk) == 0 { 253 err = ErrBadPattern 254 return 255 } 256 } 257 r, n := utf8.DecodeRuneInString(chunk) 258 if r == utf8.RuneError && n == 1 { 259 err = ErrBadPattern 260 } 261 nchunk = chunk[n:] 262 if len(nchunk) == 0 { 263 err = ErrBadPattern 264 } 265 return 266 }