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  }