github.com/boyter/gocodewalker@v1.3.2/go-gitignore/pattern.go (about) 1 // SPDX-License-Identifier: MIT 2 3 package gitignore 4 5 import ( 6 "path/filepath" 7 "strings" 8 9 "github.com/danwakefield/fnmatch" 10 ) 11 12 // Pattern represents per-line patterns within a .gitignore file 13 type Pattern interface { 14 Match 15 16 // Match returns true if the given path matches the name pattern. If the 17 // pattern is meant for directories only, and the path is not a directory, 18 // Match will return false. The matching is performed by fnmatch(). It 19 // is assumed path is relative to the base path of the owning GitIgnore. 20 Match(string, bool) bool 21 } 22 23 // pattern is the base implementation of a .gitignore pattern 24 type pattern struct { 25 _negated bool 26 _anchored bool 27 _directory bool 28 _string string 29 _fnmatch string 30 _position Position 31 } // pattern() 32 33 // name represents patterns matching a file or path name (i.e. the last 34 // component of a path) 35 type name struct { 36 pattern 37 } // name{} 38 39 // path represents a pattern that contains at least one path separator within 40 // the pattern (i.e. not at the start or end of the pattern) 41 type path struct { 42 pattern 43 _depth int 44 } // path{} 45 46 // any represents a pattern that contains at least one "any" token "**" 47 // allowing for recursive matching. 48 type any struct { 49 pattern 50 _tokens []*Token 51 } // any{} 52 53 // NewPattern returns a Pattern from the ordered slice of Tokens. The tokens are 54 // assumed to represent a well-formed .gitignore pattern. A Pattern may be 55 // negated, anchored to the start of the path (relative to the base directory 56 // of tie containing .gitignore), or match directories only. 57 func NewPattern(tokens []*Token) Pattern { 58 // if we have no tokens there is no pattern 59 if len(tokens) == 0 { 60 return nil 61 } 62 63 // extract the pattern position from first token 64 _position := tokens[0].Position 65 _string := tokenset(tokens).String() 66 67 // is this a negated pattern? 68 _negated := false 69 if tokens[0].Type == NEGATION { 70 _negated = true 71 tokens = tokens[1:] 72 } 73 74 // is this pattern anchored to the start of the path? 75 _anchored := false 76 if tokens[0].Type == SEPARATOR { 77 _anchored = true 78 tokens = tokens[1:] 79 } 80 81 // is this pattern for directories only? 82 _directory := false 83 _last := len(tokens) - 1 84 if len(tokens) != 0 { 85 if tokens[_last].Type == SEPARATOR { 86 _directory = true 87 tokens = tokens[:_last] 88 } 89 } 90 91 // build the pattern expression 92 _fnmatch := tokenset(tokens).String() 93 _pattern := &pattern{ 94 _negated: _negated, 95 _anchored: _anchored, 96 _position: _position, 97 _directory: _directory, 98 _string: _string, 99 _fnmatch: _fnmatch, 100 } 101 return _pattern.compile(tokens) 102 } // NewPattern() 103 104 // compile generates a specific Pattern (i.e. name, path or any) 105 // represented by the list of tokens. 106 func (p *pattern) compile(tokens []*Token) Pattern { 107 // what tokens do we have in this pattern? 108 // - ANY token means we can match to any depth 109 // - SEPARATOR means we have path rather than file matching 110 _separator := false 111 for _, _token := range tokens { 112 switch _token.Type { 113 case ANY: 114 return p.any(tokens) 115 case SEPARATOR: 116 _separator = true 117 } 118 } 119 120 // should we perform path or name/file matching? 121 if _separator { 122 return p.path(tokens) 123 } else { 124 return p.name(tokens) 125 } 126 } // compile() 127 128 // Ignore returns true if the pattern describes files or paths that should be 129 // ignored. 130 func (p *pattern) Ignore() bool { return !p._negated } 131 132 // Include returns true if the pattern describes files or paths that should be 133 // included (i.e. not ignored) 134 func (p *pattern) Include() bool { return p._negated } 135 136 // Position returns the position of the first token of this pattern. 137 func (p *pattern) Position() Position { return p._position } 138 139 // String returns the string representation of the pattern. 140 func (p *pattern) String() string { return p._string } 141 142 // 143 // name patterns 144 // - designed to match trailing file/directory names only 145 // 146 147 // name returns a Pattern designed to match file or directory names, with no 148 // path elements. 149 func (p *pattern) name(tokens []*Token) Pattern { 150 return &name{*p} 151 } // name() 152 153 // Match returns true if the given path matches the name pattern. If the 154 // pattern is meant for directories only, and the path is not a directory, 155 // Match will return false. The matching is performed by fnmatch(). It 156 // is assumed path is relative to the base path of the owning GitIgnore. 157 func (n *name) Match(path string, isdir bool) bool { 158 // are we expecting a directory? 159 if n._directory && !isdir { 160 return false 161 } 162 163 // should we match the whole path, or just the last component? 164 if n._anchored { 165 return fnmatch.Match(n._fnmatch, path, 0) 166 } else { 167 _, _base := filepath.Split(path) 168 return fnmatch.Match(n._fnmatch, _base, 0) 169 } 170 } // Match() 171 172 // 173 // path patterns 174 // - designed to match complete or partial paths (not just filenames) 175 // 176 177 // path returns a Pattern designed to match paths that include at least one 178 // path separator '/' neither at the end nor the start of the pattern. 179 func (p *pattern) path(tokens []*Token) Pattern { 180 // how many directory components are we expecting? 181 _depth := 0 182 for _, _token := range tokens { 183 if _token.Type == SEPARATOR { 184 _depth++ 185 } 186 } 187 188 // return the pattern instance 189 return &path{pattern: *p, _depth: _depth} 190 } // path() 191 192 // Match returns true if the given path matches the path pattern. If the 193 // pattern is meant for directories only, and the path is not a directory, 194 // Match will return false. The matching is performed by fnmatch() 195 // with flags set to FNM_PATHNAME. It is assumed path is relative to the 196 // base path of the owning GitIgnore. 197 func (p *path) Match(path string, isdir bool) bool { 198 // are we expecting a directory 199 if p._directory && !isdir { 200 return false 201 } 202 203 if fnmatch.Match(p._fnmatch, path, fnmatch.FNM_PATHNAME) { 204 return true 205 } else if p._anchored { 206 return false 207 } 208 209 // match against the trailing path elements 210 return fnmatch.Match(p._fnmatch, path, fnmatch.FNM_PATHNAME) 211 } // Match() 212 213 // 214 // "any" patterns 215 // 216 217 // any returns a Pattern designed to match paths that include at least one 218 // any pattern '**', specifying recursive matching. 219 func (p *pattern) any(tokens []*Token) Pattern { 220 // consider only the non-SEPARATOR tokens, as these will be matched 221 // against the path components 222 _tokens := make([]*Token, 0) 223 for _, _token := range tokens { 224 if _token.Type != SEPARATOR { 225 _tokens = append(_tokens, _token) 226 } 227 } 228 229 return &any{*p, _tokens} 230 } // any() 231 232 // Match returns true if the given path matches the any pattern. If the 233 // pattern is meant for directories only, and the path is not a directory, 234 // Match will return false. The matching is performed by recursively applying 235 // fnmatch() with flags set to FNM_PATHNAME. It is assumed path is relative to 236 // the base path of the owning GitIgnore. 237 func (a *any) Match(path string, isdir bool) bool { 238 // are we expecting a directory? 239 if a._directory && !isdir { 240 return false 241 } 242 243 // split the path into components 244 _parts := strings.Split(path, string(_SEPARATOR)) 245 246 // attempt to match the parts against the pattern tokens 247 return a.match(_parts, a._tokens) 248 } // Match() 249 250 // match performs the recursive matching for 'any' patterns. An 'any' 251 // token '**' may match any path component, or no path component. 252 func (a *any) match(path []string, tokens []*Token) bool { 253 // if we have no more tokens, then we have matched this path 254 // if there are also no more path elements, otherwise there's no match 255 if len(tokens) == 0 { 256 return len(path) == 0 257 } 258 259 // what token are we trying to match? 260 _token := tokens[0] 261 switch _token.Type { 262 case ANY: 263 if len(path) == 0 { 264 return a.match(path, tokens[1:]) 265 } else { 266 return a.match(path, tokens[1:]) || a.match(path[1:], tokens) 267 } 268 269 default: 270 // if we have a non-ANY token, then we must have a non-empty path 271 if len(path) != 0 { 272 // if the current path element matches this token, 273 // we match if the remainder of the path matches the 274 // remaining tokens 275 if fnmatch.Match(_token.Token(), path[0], fnmatch.FNM_PATHNAME) { 276 return a.match(path[1:], tokens[1:]) 277 } 278 } 279 } 280 281 // if we are here, then we have no match 282 return false 283 } // match() 284 285 // ensure the patterns confirm to the Pattern interface 286 var _ Pattern = &name{} 287 var _ Pattern = &path{} 288 var _ Pattern = &any{}