2016-11-21 18:38:39 +00:00
|
|
|
package filepathfilter
|
|
|
|
|
2016-11-21 21:34:45 +00:00
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"path/filepath"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
2016-11-21 23:44:41 +00:00
|
|
|
type Pattern interface {
|
2016-11-21 21:34:45 +00:00
|
|
|
Match(filename string) bool
|
|
|
|
}
|
2016-11-21 18:38:39 +00:00
|
|
|
|
|
|
|
type Filter struct {
|
2016-11-21 23:44:41 +00:00
|
|
|
include []Pattern
|
|
|
|
exclude []Pattern
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewFromPatterns(include, exclude []Pattern) *Filter {
|
|
|
|
return &Filter{include: include, exclude: exclude}
|
2016-11-21 18:38:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func New(include, exclude []string) *Filter {
|
2016-11-21 23:44:41 +00:00
|
|
|
return NewFromPatterns(convertToPatterns(include), convertToPatterns(exclude))
|
2016-11-21 18:38:39 +00:00
|
|
|
}
|
|
|
|
|
2016-11-21 21:34:45 +00:00
|
|
|
func (f *Filter) Allows(filename string) bool {
|
2016-11-21 18:38:39 +00:00
|
|
|
if f == nil {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2016-11-21 21:34:45 +00:00
|
|
|
if len(f.include)+len(f.exclude) == 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
cleanedName := filepath.Clean(filename)
|
|
|
|
|
|
|
|
if len(f.include) > 0 {
|
|
|
|
matched := false
|
|
|
|
for _, inc := range f.include {
|
|
|
|
matched = inc.Match(cleanedName)
|
|
|
|
if matched {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !matched {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(f.exclude) > 0 {
|
|
|
|
for _, ex := range f.exclude {
|
|
|
|
if ex.Match(cleanedName) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2016-11-21 23:44:41 +00:00
|
|
|
func NewPattern(rawpattern string) Pattern {
|
2016-11-21 21:57:51 +00:00
|
|
|
cleanpattern := filepath.Clean(rawpattern)
|
|
|
|
|
|
|
|
// Special case local dir, matches all (inc subpaths)
|
|
|
|
if _, local := localDirSet[cleanpattern]; local {
|
|
|
|
return noOpMatcher{}
|
|
|
|
}
|
|
|
|
|
|
|
|
hasPathSep := strings.Contains(cleanpattern, string(filepath.Separator))
|
|
|
|
|
|
|
|
// special case * when there are no path separators
|
|
|
|
// filepath.Match never allows * to match a path separator, which is correct
|
|
|
|
// for gitignore IF the pattern includes a path separator, but not otherwise
|
|
|
|
// So *.txt should match in any subdir, as should test*, but sub/*.txt would
|
|
|
|
// only match directly in the sub dir
|
|
|
|
// Don't need to test cross-platform separators as both cleaned above
|
|
|
|
if !hasPathSep && strings.Contains(cleanpattern, "*") {
|
|
|
|
pattern := regexp.QuoteMeta(cleanpattern)
|
|
|
|
regpattern := fmt.Sprintf("^%s$", strings.Replace(pattern, "\\*", ".*", -1))
|
|
|
|
return &pathlessWildcardPattern{
|
|
|
|
rawPattern: cleanpattern,
|
|
|
|
wildcardRE: regexp.MustCompile(regpattern),
|
|
|
|
}
|
|
|
|
// Also support ** with path separators
|
|
|
|
} else if hasPathSep && strings.Contains(cleanpattern, "**") {
|
|
|
|
pattern := regexp.QuoteMeta(cleanpattern)
|
|
|
|
regpattern := fmt.Sprintf("^%s$", strings.Replace(pattern, "\\*\\*", ".*", -1))
|
|
|
|
return &doubleWildcardPattern{
|
|
|
|
rawPattern: cleanpattern,
|
|
|
|
wildcardRE: regexp.MustCompile(regpattern),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return &basicPattern{rawPattern: cleanpattern}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-21 23:44:41 +00:00
|
|
|
func convertToPatterns(rawpatterns []string) []Pattern {
|
|
|
|
patterns := make([]Pattern, len(rawpatterns))
|
2016-11-21 21:34:45 +00:00
|
|
|
for i, raw := range rawpatterns {
|
2016-11-21 23:44:41 +00:00
|
|
|
patterns[i] = NewPattern(raw)
|
2016-11-21 21:34:45 +00:00
|
|
|
}
|
|
|
|
return patterns
|
|
|
|
}
|
|
|
|
|
|
|
|
type basicPattern struct {
|
|
|
|
rawPattern string
|
2016-11-21 18:38:39 +00:00
|
|
|
}
|
2016-11-21 21:34:45 +00:00
|
|
|
|
|
|
|
// Match is a revised version of filepath.Match which makes it behave more
|
|
|
|
// like gitignore
|
|
|
|
func (p *basicPattern) Match(name string) bool {
|
|
|
|
matched, _ := filepath.Match(p.rawPattern, name)
|
|
|
|
// Also support matching a parent directory without a wildcard
|
|
|
|
return matched || strings.HasPrefix(name, p.rawPattern+string(filepath.Separator))
|
|
|
|
}
|
|
|
|
|
|
|
|
type pathlessWildcardPattern struct {
|
|
|
|
rawPattern string
|
|
|
|
wildcardRE *regexp.Regexp
|
|
|
|
}
|
|
|
|
|
|
|
|
// Match is a revised version of filepath.Match which makes it behave more
|
|
|
|
// like gitignore
|
|
|
|
func (p *pathlessWildcardPattern) Match(name string) bool {
|
|
|
|
matched, _ := filepath.Match(p.rawPattern, name)
|
|
|
|
// Match the whole of the base name but allow matching in folders if no path
|
|
|
|
return matched || p.wildcardRE.MatchString(filepath.Base(name))
|
|
|
|
}
|
|
|
|
|
|
|
|
type doubleWildcardPattern struct {
|
|
|
|
rawPattern string
|
|
|
|
wildcardRE *regexp.Regexp
|
|
|
|
}
|
|
|
|
|
|
|
|
// Match is a revised version of filepath.Match which makes it behave more
|
|
|
|
// like gitignore
|
|
|
|
func (p *doubleWildcardPattern) Match(name string) bool {
|
|
|
|
matched, _ := filepath.Match(p.rawPattern, name)
|
|
|
|
// Match the whole of the base name but allow matching in folders if no path
|
|
|
|
return matched || p.wildcardRE.MatchString(name)
|
|
|
|
}
|
|
|
|
|
|
|
|
type noOpMatcher struct {
|
|
|
|
}
|
|
|
|
|
|
|
|
func (n noOpMatcher) Match(name string) bool {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2016-11-21 23:34:35 +00:00
|
|
|
var localDirSet = map[string]struct{}{
|
|
|
|
".": struct{}{},
|
|
|
|
"./": struct{}{},
|
|
|
|
".\\": struct{}{},
|
|
|
|
}
|