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