From 18e4d62b2daad3deff232d29b38feccd157659d0 Mon Sep 17 00:00:00 2001 From: Bob Matcuk Date: Sun, 1 Dec 2019 16:04:02 -0500 Subject: [PATCH] support for path seps inside {alts} --- doublestar.go | 365 +++++++++++++++++++++++++++++++-------------- doublestar_test.go | 222 ++++++++++++++------------- 2 files changed, 365 insertions(+), 222 deletions(-) diff --git a/doublestar.go b/doublestar.go index 0044dfa..d600307 100644 --- a/doublestar.go +++ b/doublestar.go @@ -72,6 +72,101 @@ func indexRuneWithEscaping(s string, r rune) int { return end } +// Find the last index of a rune in a string, +// ignoring any times the rune is escaped using "\". +func lastIndexRuneWithEscaping(s string, r rune) int { + end := strings.LastIndex(s, string(r)) + if end == -1 { + return -1 + } + if end > 0 && s[end-1] == '\\' { + end = lastIndexRuneWithEscaping(s[:end-1], r) + } + return end +} + +// Find the index of the first instance of one of the unicode characters in +// chars, ignoring any times those characters are escaped using "\". +func indexAnyWithEscaping(s, chars string) int { + end := strings.IndexAny(s, chars) + if end == -1 { + return -1 + } + if end > 0 && s[end-1] == '\\' { + _, adj := utf8.DecodeRuneInString(s[end:]) + start := end + adj + end = indexAnyWithEscaping(s[start:], chars) + if end != -1 { + end += start + } + } + return end +} + +// Split a set of alternatives such as {alt1,alt2,...} and returns the index of +// the rune after the closing curly brace. Respects nested alternatives and +// escaped runes. +func splitAlternatives(s string) (ret []string, idx int) { + ret = make([]string, 0, 2) + idx = 0 + slen := len(s) + braceCnt := 1 + esc := false + start := 0 + for braceCnt > 0 { + if idx >= slen { + return nil, -1 + } + + sRune, adj := utf8.DecodeRuneInString(s[idx:]) + if esc { + esc = false + } else if sRune == '\\' { + esc = true + } else if sRune == '{' { + braceCnt++ + } else if sRune == '}' { + braceCnt-- + } else if sRune == ',' && braceCnt == 1 { + ret = append(ret, s[start:idx]) + start = idx + adj + } + + idx += adj + } + ret = append(ret, s[start:idx-1]) + return +} + +// Returns true if the pattern is "zero length", meaning +// it could match zero or more characters. +func isZeroLengthPattern(pattern string) (ret bool, err error) { + // * can match zero + if pattern == "" || pattern == "*" || pattern == "**" { + return true, nil + } + + // an alternative with zero length can match zero, for example {,x} - the + // first alternative has zero length + r, adj := utf8.DecodeRuneInString(pattern) + if r == '{' { + options, endOptions := splitAlternatives(pattern[adj:]) + if endOptions == -1 { + return false, ErrBadPattern + } + if ret, err = isZeroLengthPattern(pattern[adj+endOptions:]); !ret || err != nil { + return + } + for _, o := range options { + if ret, err = isZeroLengthPattern(o); ret || err != nil { + return + } + } + } + + return false, nil +} + // Match returns true if name matches the shell file name pattern. // The pattern syntax is: // @@ -114,6 +209,7 @@ func Match(pattern, name string) (bool, error) { // Note: this is meant as a drop-in replacement for filepath.Match(). // func PathMatch(pattern, name string) (bool, error) { + pattern = filepath.ToSlash(pattern) return matchWithSeparator(pattern, name, os.PathSeparator) } @@ -143,14 +239,13 @@ func PathMatch(pattern, name string) (bool, error) { // is malformed. // func matchWithSeparator(pattern, name string, separator rune) (bool, error) { - patternComponents := splitPathOnSeparator(pattern, separator) nameComponents := splitPathOnSeparator(name, separator) - return doMatching(patternComponents, nameComponents) + return doMatching(pattern, nameComponents) } -func doMatching(patternComponents, nameComponents []string) (matched bool, err error) { +func doMatching(pattern string, nameComponents []string) (matched bool, err error) { // check for some base-cases - patternLen, nameLen := len(patternComponents), len(nameComponents) + patternLen, nameLen := len(pattern), len(nameComponents) if patternLen == 0 && nameLen == 0 { return true, nil } @@ -158,34 +253,46 @@ func doMatching(patternComponents, nameComponents []string) (matched bool, err e return false, nil } - patIdx, nameIdx := 0, 0 - for patIdx < patternLen && nameIdx < nameLen { - if patternComponents[patIdx] == "**" { - // if our last pattern component is a doublestar, we're done - - // doublestar will match any remaining name components, if any. - if patIdx++; patIdx >= patternLen { - return true, nil - } + slashIdx := indexRuneWithEscaping(pattern, '/') + lastComponent := slashIdx == -1 + if lastComponent { + slashIdx = len(pattern) + } + if pattern[:slashIdx] == "**" { + // if our last pattern component is a doublestar, we're done - + // doublestar will match any remaining name components, if any. + if lastComponent { + return true, nil + } - // otherwise, try matching remaining components - for ; nameIdx < nameLen; nameIdx++ { - if m, _ := doMatching(patternComponents[patIdx:], nameComponents[nameIdx:]); m { - return true, nil - } + // otherwise, try matching remaining components + for nameIdx := 0; nameIdx < nameLen; nameIdx++ { + if m, _ := doMatching(pattern[slashIdx+1:], nameComponents[nameIdx:]); m { + return true, nil } - return false, nil } + return false, nil + } - // try matching components - matched, err = matchComponent(patternComponents[patIdx], nameComponents[nameIdx]) - if !matched || err != nil { - return - } + var matches []string + matches, err = matchComponent(pattern, nameComponents[0]) + if matches == nil || err != nil { + return + } + if len(matches) == 0 && nameLen == 1 { + return true, nil + } - patIdx++ - nameIdx++ + if nameLen > 1 { + for _, alt := range matches { + matched, err = doMatching(alt, nameComponents[1:]) + if matched || err != nil { + return + } + } } - return patIdx >= patternLen && nameIdx >= nameLen, nil + + return false, nil } // Glob returns the names of all files matching pattern or nil @@ -204,48 +311,43 @@ func doMatching(patternComponents, nameComponents []string) (matched bool, err e // Note: this is meant as a drop-in replacement for filepath.Glob(). // func Glob(pattern string) (matches []string, err error) { - patternComponents := splitPathOnSeparator(filepath.ToSlash(pattern), '/') - if len(patternComponents) == 0 { + if len(pattern) == 0 { return nil, nil } - // On Windows systems, this will return the drive name ('C:') for filesystem - // paths, or \\\ for UNC paths. On other systems, it will - // return an empty string. Since absolute paths on non-Windows systems start - // with a slash, patternComponent[0] == volumeName will return true for both - // absolute Windows paths and absolute non-Windows paths, but we need a - // separate check for UNC paths. + // If the pattern is relative or absolute and we're on a non-Windows machine, + // volumeName will be an empty string. If it is absolute and we're on a + // Windows machine, volumeName will be a drive letter ("C:") for filesystem + // paths or \\\ for UNC paths. + isAbs := filepath.IsAbs(pattern) volumeName := filepath.VolumeName(pattern) - isWindowsUNC := strings.HasPrefix(pattern, `\\`) - if isWindowsUNC || patternComponents[0] == volumeName { - startComponentIndex := 1 - if isWindowsUNC { - startComponentIndex = 4 - } - return doGlob(fmt.Sprintf("%s%s", volumeName, string(os.PathSeparator)), patternComponents[startComponentIndex:], matches) + isWindowsUNC := strings.HasPrefix(volumeName, `\\`) + if isWindowsUNC || isAbs { + startIdx := len(volumeName) + 1 + return doGlob(fmt.Sprintf("%s%s", volumeName, string(os.PathSeparator)), filepath.ToSlash(pattern[startIdx:]), matches) } // otherwise, it's a relative pattern - return doGlob(".", patternComponents, matches) + return doGlob(".", filepath.ToSlash(pattern), matches) } // Perform a glob -func doGlob(basedir string, components, matches []string) (m []string, e error) { +func doGlob(basedir, pattern string, matches []string) (m []string, e error) { m = matches e = nil - // figure out how many components we don't need to glob because they're - // just names without patterns - we'll use os.Lstat below to check if that - // path actually exists - patLen := len(components) - patIdx := 0 - for ; patIdx < patLen; patIdx++ { - if strings.IndexAny(components[patIdx], "*?[{\\") >= 0 { - break - } + // if the pattern starts with any path components that aren't globbed (ie, + // `path/to/glob*`), we can skip over the un-globbed components (`path/to` in + // our example). + globIdx := indexAnyWithEscaping(pattern, "*?[{\\") + if globIdx > 0 { + globIdx = lastIndexRuneWithEscaping(pattern[:globIdx], '/') + } else if globIdx == -1 { + globIdx = lastIndexRuneWithEscaping(pattern, '/') } - if patIdx > 0 { - basedir = filepath.Join(basedir, filepath.Join(components[0:patIdx]...)) + if globIdx > 0 { + basedir = filepath.Join(basedir, pattern[:globIdx]) + pattern = pattern[globIdx+1:] } // Lstat will return an error if the file/directory doesn't exist @@ -254,8 +356,8 @@ func doGlob(basedir string, components, matches []string) (m []string, e error) return } - // if there are no more components, we've found a match - if patIdx >= patLen { + // if the pattern is empty, we've found a match + if len(pattern) == 0 { m = append(m, basedir) return } @@ -282,8 +384,12 @@ func doGlob(basedir string, components, matches []string) (m []string, e error) defer dir.Close() files, _ := dir.Readdir(-1) - lastComponent := (patIdx + 1) >= patLen - if components[patIdx] == "**" { + slashIdx := indexRuneWithEscaping(pattern, '/') + lastComponent := slashIdx == -1 + if lastComponent { + slashIdx = len(pattern) + } + if pattern[:slashIdx] == "**" { // if the current component is a doublestar, we'll try depth-first for _, file := range files { // if symlink, we may want to follow @@ -299,7 +405,7 @@ func doGlob(basedir string, components, matches []string) (m []string, e error) if lastComponent { m = append(m, filepath.Join(basedir, file.Name())) } - m, e = doGlob(filepath.Join(basedir, file.Name()), components[patIdx:], m) + m, e = doGlob(filepath.Join(basedir, file.Name()), pattern, m) } else if lastComponent { // if the pattern's last component is a doublestar, we match filenames, too m = append(m, filepath.Join(basedir, file.Name())) @@ -308,80 +414,81 @@ func doGlob(basedir string, components, matches []string) (m []string, e error) if lastComponent { return // we're done } - patIdx++ - lastComponent = (patIdx + 1) >= patLen + + pattern = pattern[slashIdx+1:] } // check items in current directory and recurse - var match bool + var match []string for _, file := range files { - match, e = matchComponent(components[patIdx], file.Name()) + match, e = matchComponent(pattern, file.Name()) if e != nil { return } - if match { - if lastComponent { + if match != nil { + if len(match) == 0 { m = append(m, filepath.Join(basedir, file.Name())) } else { - m, e = doGlob(filepath.Join(basedir, file.Name()), components[patIdx+1:], m) + for _, alt := range match { + m, e = doGlob(filepath.Join(basedir, file.Name()), alt, m) + } } } } return } -// Attempt to match a single pattern component with a path component -func matchComponent(pattern, name string) (bool, error) { - // check some base cases - patternLen, nameLen := len(pattern), len(name) - if patternLen == 0 && nameLen == 0 { - return true, nil - } - if patternLen == 0 { - return false, nil - } - if nameLen == 0 && pattern != "*" { - return false, nil - } - +// Attempt to match a single path component with a pattern. Note that the +// pattern may include multiple components but that the "name" is just a single +// path component. The return value is a slice of patterns that should be +// checked against subsequent path components or nil, indicating that the +// pattern does not match this path. It is assumed that pattern components are +// separated by '/' +func matchComponent(pattern, name string) ([]string, error) { // check for matches one rune at a time + patternLen, nameLen := len(pattern), len(name) patIdx, nameIdx := 0, 0 for patIdx < patternLen && nameIdx < nameLen { patRune, patAdj := utf8.DecodeRuneInString(pattern[patIdx:]) nameRune, nameAdj := utf8.DecodeRuneInString(name[nameIdx:]) - if patRune == '\\' { - // handle escaped runes + if patRune == '/' { + patIdx++ + break + } else if patRune == '\\' { + // handle escaped runes, only if separator isn't '\\' patIdx += patAdj patRune, patAdj = utf8.DecodeRuneInString(pattern[patIdx:]) if patRune == utf8.RuneError { - return false, ErrBadPattern + return nil, ErrBadPattern } else if patRune == nameRune { patIdx += patAdj nameIdx += nameAdj } else { - return false, nil + return nil, nil } } else if patRune == '*' { - // handle stars + // handle stars - a star at the end of the pattern or before a separator + // will always match the rest of the path component if patIdx += patAdj; patIdx >= patternLen { - // a star at the end of a pattern will always - // match the rest of the path - return true, nil + return []string{}, nil + } + if patRune, patAdj = utf8.DecodeRuneInString(pattern[patIdx:]); patRune == '/' { + return []string{pattern[patIdx+patAdj:]}, nil } // check if we can make any matches for ; nameIdx < nameLen; nameIdx += nameAdj { - if m, _ := matchComponent(pattern[patIdx:], name[nameIdx:]); m { - return true, nil + if m, e := matchComponent(pattern[patIdx:], name[nameIdx:]); m != nil || e != nil { + return m, e } } - return false, nil + return nil, nil } else if patRune == '[' { // handle character sets patIdx += patAdj endClass := indexRuneWithEscaping(pattern[patIdx:], ']') if endClass == -1 { - return false, ErrBadPattern + return nil, ErrBadPattern } endClass += patIdx classRunes := []rune(pattern[patIdx:endClass]) @@ -395,7 +502,7 @@ func matchComponent(pattern, name string) (bool, error) { for classIdx < classRunesLen { low := classRunes[classIdx] if low == '-' { - return false, ErrBadPattern + return nil, ErrBadPattern } classIdx++ if low == '\\' { @@ -403,18 +510,18 @@ func matchComponent(pattern, name string) (bool, error) { low = classRunes[classIdx] classIdx++ } else { - return false, ErrBadPattern + return nil, ErrBadPattern } } high := low if classIdx < classRunesLen && classRunes[classIdx] == '-' { // we have a range of runes if classIdx++; classIdx >= classRunesLen { - return false, ErrBadPattern + return nil, ErrBadPattern } high = classRunes[classIdx] if high == '-' { - return false, ErrBadPattern + return nil, ErrBadPattern } classIdx++ if high == '\\' { @@ -422,7 +529,7 @@ func matchComponent(pattern, name string) (bool, error) { high = classRunes[classIdx] classIdx++ } else { - return false, ErrBadPattern + return nil, ErrBadPattern } } } @@ -431,46 +538,74 @@ func matchComponent(pattern, name string) (bool, error) { } } if matchClass == (classRunes[0] == '^') { - return false, nil + return nil, nil } } else { - return false, ErrBadPattern + return nil, ErrBadPattern } patIdx = endClass + 1 nameIdx += nameAdj } else if patRune == '{' { // handle alternatives such as {alt1,alt2,...} patIdx += patAdj - endOptions := indexRuneWithEscaping(pattern[patIdx:], '}') + options, endOptions := splitAlternatives(pattern[patIdx:]) if endOptions == -1 { - return false, ErrBadPattern + return nil, ErrBadPattern } - endOptions += patIdx - options := splitPathOnSeparator(pattern[patIdx:endOptions], ',') - patIdx = endOptions + 1 + patIdx += endOptions + + results := make([][]string, 0, len(options)) + totalResults := 0 for _, o := range options { m, e := matchComponent(o+pattern[patIdx:], name[nameIdx:]) if e != nil { - return false, e + return nil, e + } + if m != nil { + results = append(results, m) + totalResults += len(m) } - if m { - return true, nil + } + if len(results) > 0 { + lst := make([]string, 0, totalResults) + for _, m := range results { + lst = append(lst, m...) } + return lst, nil } - return false, nil + + return nil, nil } else if patRune == '?' || patRune == nameRune { // handle single-rune wildcard patIdx += patAdj nameIdx += nameAdj } else { - return false, nil + return nil, nil } } - if patIdx >= patternLen && nameIdx >= nameLen { - return true, nil - } - if nameIdx >= nameLen && pattern[patIdx:] == "*" || pattern[patIdx:] == "**" { - return true, nil + if nameIdx >= nameLen { + if patIdx >= patternLen { + return []string{}, nil + } + + pattern = pattern[patIdx:] + slashIdx := indexRuneWithEscaping(pattern, '/') + testPattern := pattern + if slashIdx >= 0 { + testPattern = pattern[:slashIdx] + } + + zeroLength, err := isZeroLengthPattern(testPattern) + if err != nil { + return nil, err + } + if zeroLength { + if slashIdx == -1 { + return []string{}, nil + } else { + return []string{pattern[slashIdx+1:]}, nil + } + } } - return false, nil + return nil, nil } diff --git a/doublestar_test.go b/doublestar_test.go index 77d388d..213528a 100644 --- a/doublestar_test.go +++ b/doublestar_test.go @@ -13,100 +13,112 @@ import ( ) type MatchTest struct { - pattern, testPath []string // a pattern and path to test the pattern on - shouldMatch bool // true if the pattern should match the path - expectedErr error // an expected error - testOnDisk bool // true: test pattern against files in "test" directory + pattern, testPath string // a pattern and path to test the pattern on + shouldMatch bool // true if the pattern should match the path + expectedErr error // an expected error + testOnDisk bool // true: test pattern against files in "test" directory } // Tests which contain escapes and symlinks will not work on Windows var onWindows = runtime.GOOS == "windows" var matchTests = []MatchTest{ - {[]string{"*"}, []string{""}, true, nil, false}, - {[]string{"*"}, []string{"/"}, false, nil, false}, - {[]string{"/*"}, []string{"/"}, true, nil, false}, - {[]string{"/*"}, []string{"/debug/"}, false, nil, false}, - {[]string{"/*"}, []string{"//"}, false, nil, false}, - {[]string{"abc"}, []string{"abc"}, true, nil, true}, - {[]string{"*"}, []string{"abc"}, true, nil, true}, - {[]string{"*c"}, []string{"abc"}, true, nil, true}, - {[]string{"a*"}, []string{"a"}, true, nil, true}, - {[]string{"a*"}, []string{"abc"}, true, nil, true}, - {[]string{"a*"}, []string{"ab", "c"}, false, nil, true}, - {[]string{"a*", "b"}, []string{"abc", "b"}, true, nil, true}, - {[]string{"a*", "b"}, []string{"a", "c", "b"}, false, nil, true}, - {[]string{"a*b*c*d*e*", "f"}, []string{"axbxcxdxe", "f"}, true, nil, true}, - {[]string{"a*b*c*d*e*", "f"}, []string{"axbxcxdxexxx", "f"}, true, nil, true}, - {[]string{"a*b*c*d*e*", "f"}, []string{"axbxcxdxe", "xxx", "f"}, false, nil, true}, - {[]string{"a*b*c*d*e*", "f"}, []string{"axbxcxdxexxx", "fff"}, false, nil, true}, - {[]string{"a*b?c*x"}, []string{"abxbbxdbxebxczzx"}, true, nil, true}, - {[]string{"a*b?c*x"}, []string{"abxbbxdbxebxczzy"}, false, nil, true}, - {[]string{"ab[c]"}, []string{"abc"}, true, nil, true}, - {[]string{"ab[b-d]"}, []string{"abc"}, true, nil, true}, - {[]string{"ab[e-g]"}, []string{"abc"}, false, nil, true}, - {[]string{"ab[^c]"}, []string{"abc"}, false, nil, true}, - {[]string{"ab[^b-d]"}, []string{"abc"}, false, nil, true}, - {[]string{"ab[^e-g]"}, []string{"abc"}, true, nil, true}, - {[]string{"a\\*b"}, []string{"ab"}, false, nil, true}, - {[]string{"a?b"}, []string{"a☺b"}, true, nil, true}, - {[]string{"a[^a]b"}, []string{"a☺b"}, true, nil, true}, - {[]string{"a???b"}, []string{"a☺b"}, false, nil, true}, - {[]string{"a[^a][^a][^a]b"}, []string{"a☺b"}, false, nil, true}, - {[]string{"[a-ζ]*"}, []string{"α"}, true, nil, true}, - {[]string{"*[a-ζ]"}, []string{"A"}, false, nil, true}, - {[]string{"a?b"}, []string{"a", "b"}, false, nil, true}, - {[]string{"a*b"}, []string{"a", "b"}, false, nil, true}, - {[]string{"[\\]a]"}, []string{"]"}, true, nil, !onWindows}, - {[]string{"[\\-]"}, []string{"-"}, true, nil, !onWindows}, - {[]string{"[x\\-]"}, []string{"x"}, true, nil, !onWindows}, - {[]string{"[x\\-]"}, []string{"-"}, true, nil, !onWindows}, - {[]string{"[x\\-]"}, []string{"z"}, false, nil, !onWindows}, - {[]string{"[\\-x]"}, []string{"x"}, true, nil, !onWindows}, - {[]string{"[\\-x]"}, []string{"-"}, true, nil, !onWindows}, - {[]string{"[\\-x]"}, []string{"a"}, false, nil, !onWindows}, - {[]string{"[]a]"}, []string{"]"}, false, ErrBadPattern, true}, - {[]string{"[-]"}, []string{"-"}, false, ErrBadPattern, true}, - {[]string{"[x-]"}, []string{"x"}, false, ErrBadPattern, true}, - {[]string{"[x-]"}, []string{"-"}, false, ErrBadPattern, true}, - {[]string{"[x-]"}, []string{"z"}, false, ErrBadPattern, true}, - {[]string{"[-x]"}, []string{"x"}, false, ErrBadPattern, true}, - {[]string{"[-x]"}, []string{"-"}, false, ErrBadPattern, true}, - {[]string{"[-x]"}, []string{"a"}, false, ErrBadPattern, true}, - {[]string{"\\"}, []string{"a"}, false, ErrBadPattern, !onWindows}, - {[]string{"[a-b-c]"}, []string{"a"}, false, ErrBadPattern, true}, - {[]string{"["}, []string{"a"}, false, ErrBadPattern, true}, - {[]string{"[^"}, []string{"a"}, false, ErrBadPattern, true}, - {[]string{"[^bc"}, []string{"a"}, false, ErrBadPattern, true}, - {[]string{"a["}, []string{"a"}, false, nil, false}, - {[]string{"a["}, []string{"ab"}, false, ErrBadPattern, true}, - {[]string{"*x"}, []string{"xxx"}, true, nil, true}, - {[]string{"[abc]"}, []string{"b"}, true, nil, true}, - {[]string{"a", "**"}, []string{"a"}, false, nil, true}, - {[]string{"a", "**"}, []string{"a", "b"}, true, nil, true}, - {[]string{"a", "**"}, []string{"a", "b", "c"}, true, nil, true}, - {[]string{"**", "c"}, []string{"c"}, true, nil, true}, - {[]string{"**", "c"}, []string{"b", "c"}, true, nil, true}, - {[]string{"**", "c"}, []string{"a", "b", "c"}, true, nil, true}, - {[]string{"**", "c"}, []string{"a", "b"}, false, nil, true}, - {[]string{"**", "c"}, []string{"abcd"}, false, nil, true}, - {[]string{"**", "c"}, []string{"a", "abc"}, false, nil, true}, - {[]string{"a", "**", "b"}, []string{"a", "b"}, true, nil, true}, - {[]string{"a", "**", "c"}, []string{"a", "b", "c"}, true, nil, true}, - {[]string{"a", "**", "d"}, []string{"a", "b", "c", "d"}, true, nil, true}, - {[]string{"a", "\\**"}, []string{"a", "b", "c"}, false, nil, !onWindows}, - {[]string{"a", "", "b", "c"}, []string{"a", "b", "c"}, true, nil, true}, - {[]string{"a", "b", "c"}, []string{"a", "b", "", "c"}, true, nil, true}, - {[]string{"ab{c,d}"}, []string{"abc"}, true, nil, true}, - {[]string{"ab{c,d,*}"}, []string{"abcde"}, true, nil, true}, - {[]string{"ab{c,d}["}, []string{"abcd"}, false, ErrBadPattern, true}, - {[]string{"abc", "**"}, []string{"abc", "b"}, true, nil, true}, - {[]string{"**", "abc"}, []string{"abc"}, true, nil, true}, - {[]string{"abc**"}, []string{"abc", "b"}, false, nil, true}, - {[]string{"broken-symlink"}, []string{"broken-symlink"}, true, nil, !onWindows}, - {[]string{"working-symlink", "c", "*"}, []string{"working-symlink", "c", "d"}, true, nil, !onWindows}, - {[]string{"working-sym*", "*"}, []string{"working-symlink", "c"}, true, nil, !onWindows}, - {[]string{"b", "**", "f"}, []string{"b", "symlink-dir", "f"}, true, nil, !onWindows}, + {"*", "", true, nil, false}, + {"*", "/", false, nil, false}, + {"/*", "/", true, nil, false}, + {"/*", "/debug/", false, nil, false}, + {"/*", "//", false, nil, false}, + {"abc", "abc", true, nil, true}, + {"*", "abc", true, nil, true}, + {"*c", "abc", true, nil, true}, + {"a*", "a", true, nil, true}, + {"a*", "abc", true, nil, true}, + {"a*", "ab/c", false, nil, true}, + {"a*/b", "abc/b", true, nil, true}, + {"a*/b", "a/c/b", false, nil, true}, + {"a*b*c*d*e*", "axbxcxdxe", true, nil, true}, + {"a*b*c*d*e*/f", "axbxcxdxe/f", true, nil, true}, + {"a*b*c*d*e*/f", "axbxcxdxexxx/f", true, nil, true}, + {"a*b*c*d*e*/f", "axbxcxdxe/xxx/f", false, nil, true}, + {"a*b*c*d*e*/f", "axbxcxdxexxx/fff", false, nil, true}, + {"a*b?c*x", "abxbbxdbxebxczzx", true, nil, true}, + {"a*b?c*x", "abxbbxdbxebxczzy", false, nil, true}, + {"ab[c]", "abc", true, nil, true}, + {"ab[b-d]", "abc", true, nil, true}, + {"ab[e-g]", "abc", false, nil, true}, + {"ab[^c]", "abc", false, nil, true}, + {"ab[^b-d]", "abc", false, nil, true}, + {"ab[^e-g]", "abc", true, nil, true}, + {"a\\*b", "ab", false, nil, true}, + {"a?b", "a☺b", true, nil, true}, + {"a[^a]b", "a☺b", true, nil, true}, + {"a???b", "a☺b", false, nil, true}, + {"a[^a][^a][^a]b", "a☺b", false, nil, true}, + {"[a-ζ]*", "α", true, nil, true}, + {"*[a-ζ]", "A", false, nil, true}, + {"a?b", "a/b", false, nil, true}, + {"a*b", "a/b", false, nil, true}, + {"[\\]a]", "]", true, nil, !onWindows}, + {"[\\-]", "-", true, nil, !onWindows}, + {"[x\\-]", "x", true, nil, !onWindows}, + {"[x\\-]", "-", true, nil, !onWindows}, + {"[x\\-]", "z", false, nil, !onWindows}, + {"[\\-x]", "x", true, nil, !onWindows}, + {"[\\-x]", "-", true, nil, !onWindows}, + {"[\\-x]", "a", false, nil, !onWindows}, + {"[]a]", "]", false, ErrBadPattern, true}, + {"[-]", "-", false, ErrBadPattern, true}, + {"[x-]", "x", false, ErrBadPattern, true}, + {"[x-]", "-", false, ErrBadPattern, true}, + {"[x-]", "z", false, ErrBadPattern, true}, + {"[-x]", "x", false, ErrBadPattern, true}, + {"[-x]", "-", false, ErrBadPattern, true}, + {"[-x]", "a", false, ErrBadPattern, true}, + {"\\", "a", false, ErrBadPattern, !onWindows}, + {"[a-b-c]", "a", false, ErrBadPattern, true}, + {"[", "a", false, ErrBadPattern, true}, + {"[^", "a", false, ErrBadPattern, true}, + {"[^bc", "a", false, ErrBadPattern, true}, + {"a[", "a", false, nil, false}, + {"a[", "ab", false, ErrBadPattern, true}, + {"*x", "xxx", true, nil, true}, + {"[abc]", "b", true, nil, true}, + {"a/**", "a", false, nil, true}, + {"a/**", "a/b", true, nil, true}, + {"a/**", "a/b/c", true, nil, true}, + {"**/c", "c", true, nil, true}, + {"**/c", "b/c", true, nil, true}, + {"**/c", "a/b/c", true, nil, true}, + {"**/c", "a/b", false, nil, true}, + {"**/c", "abcd", false, nil, true}, + {"**/c", "a/abc", false, nil, true}, + {"a/**/b", "a/b", true, nil, true}, + {"a/**/c", "a/b/c", true, nil, true}, + {"a/**/d", "a/b/c/d", true, nil, true}, + {"a/\\**", "a/b/c", false, nil, !onWindows}, + // this is an odd case: filepath.Glob() will return results + {"a//b/c", "a/b/c", false, nil, false}, + {"a/b/c", "a/b//c", false, nil, true}, + // also odd: Glob + filepath.Glob return results + {"a/", "a", false, nil, false}, + {"ab{c,d}", "abc", true, nil, true}, + {"ab{c,d,*}", "abcde", true, nil, true}, + {"ab{c,d}[", "abcd", false, ErrBadPattern, true}, + {"a{,bc}", "a", true, nil, true}, + {"a{,bc}", "abc", true, nil, true}, + {"a/{b/c,c/b}", "a/b/c", true, nil, true}, + {"a/{b/c,c/b}", "a/c/b", true, nil, true}, + {"{a/{b,c},abc}", "a/b", true, nil, true}, + {"{a/{b,c},abc}", "a/c", true, nil, true}, + {"{a/{b,c},abc}", "abc", true, nil, true}, + {"{a/{b,c},abc}", "a/b/c", false, nil, true}, + {"abc/**", "abc/b", true, nil, true}, + {"**/abc", "abc", true, nil, true}, + {"abc**", "abc/b", false, nil, true}, + {"broken-symlink", "broken-symlink", true, nil, !onWindows}, + {"working-symlink/c/*", "working-symlink/c/d", true, nil, !onWindows}, + {"working-sym*/*", "working-symlink/c", true, nil, !onWindows}, + {"b/**/f", "b/symlink-dir/f", true, nil, !onWindows}, } func TestMatch(t *testing.T) { @@ -125,24 +137,15 @@ func testMatchWith(t *testing.T, idx int, tt MatchTest) { }() // Match() always uses "/" as the separator - pattern := tt.pattern[0] - testPath := tt.testPath[0] - if len(tt.pattern) > 1 { - pattern = path.Join(tt.pattern...) - } - if len(tt.testPath) > 1 { - testPath = path.Join(tt.testPath...) - } - - ok, err := Match(pattern, testPath) + ok, err := Match(tt.pattern, tt.testPath) if ok != tt.shouldMatch || err != tt.expectedErr { - t.Errorf("#%v. Match(%#q, %#q) = %v, %v want %v, %v", idx, pattern, testPath, ok, err, tt.shouldMatch, tt.expectedErr) + t.Errorf("#%v. Match(%#q, %#q) = %v, %v want %v, %v", idx, tt.pattern, tt.testPath, ok, err, tt.shouldMatch, tt.expectedErr) } - if isStandardPattern(pattern) { - stdOk, stdErr := path.Match(pattern, testPath) + if isStandardPattern(tt.pattern) { + stdOk, stdErr := path.Match(tt.pattern, tt.testPath) if ok != stdOk || !compareErrors(err, stdErr) { - t.Errorf("#%v. Match(%#q, %#q) != path.Match(...). Got %v, %v want %v, %v", idx, pattern, testPath, ok, err, stdOk, stdErr) + t.Errorf("#%v. Match(%#q, %#q) != path.Match(...). Got %v, %v want %v, %v", idx, tt.pattern, tt.testPath, ok, err, stdOk, stdErr) } } } @@ -166,8 +169,8 @@ func testPathMatchWith(t *testing.T, idx int, tt MatchTest) { } }() - pattern := filepath.Join(tt.pattern...) - testPath := filepath.Join(tt.testPath...) + pattern := filepath.FromSlash(tt.pattern) + testPath := filepath.FromSlash(tt.testPath) ok, err := PathMatch(pattern, testPath) if ok != tt.shouldMatch || err != tt.expectedErr { t.Errorf("#%v. Match(%#q, %#q) = %v, %v want %v, %v", idx, pattern, testPath, ok, err, tt.shouldMatch, tt.expectedErr) @@ -188,10 +191,11 @@ func TestGlob(t *testing.T) { return } + abspath = filepath.Join(abspath, "test") for idx, tt := range matchTests { if tt.testOnDisk { // test both relative paths and absolute paths - testGlobWith(t, idx, tt, "") + testGlobWith(t, idx, tt, "test") testGlobWith(t, idx, tt, abspath) } } @@ -204,8 +208,8 @@ func testGlobWith(t *testing.T, idx int, tt MatchTest, basepath string) { } }() - pattern := filepath.Join(basepath, "test", filepath.Join(tt.pattern...)) - testPath := filepath.Join(basepath, "test", filepath.Join(tt.testPath...)) + pattern := joinWithoutClean(basepath, filepath.FromSlash(tt.pattern)) + testPath := joinWithoutClean(basepath, filepath.FromSlash(tt.testPath)) matches, err := Glob(pattern) if inSlice(testPath, matches) != tt.shouldMatch { if tt.shouldMatch { @@ -226,6 +230,10 @@ func testGlobWith(t *testing.T, idx int, tt MatchTest, basepath string) { } } +func joinWithoutClean(elem ...string) string { + return strings.Join(elem, string(os.PathSeparator)) +} + func isStandardPattern(pattern string) bool { return !strings.Contains(pattern, "**") && indexRuneWithEscaping(pattern, '{') == -1 }