pax_global_header00006660000000000000000000000064135712751350014523gustar00rootroot0000000000000052 comment=8fcc08761109c1a6699c22f50a73448182507b32 doublestar-1.2.1/000077500000000000000000000000001357127513500136705ustar00rootroot00000000000000doublestar-1.2.1/.gitignore000066400000000000000000000004671357127513500156670ustar00rootroot00000000000000# vi *~ *.swp *.swo # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof # test directory test/ doublestar-1.2.1/.travis.yml000066400000000000000000000003211357127513500157750ustar00rootroot00000000000000language: go go: - 1.11 - 1.12 before_install: - go get -t -v ./... script: - go test -race -coverprofile=coverage.txt -covermode=atomic after_success: - bash <(curl -s https://codecov.io/bash) doublestar-1.2.1/LICENSE000066400000000000000000000020661357127513500147010ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 Bob Matcuk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. doublestar-1.2.1/README.md000066400000000000000000000072011357127513500151470ustar00rootroot00000000000000![Release](https://img.shields.io/github/release/bmatcuk/doublestar.svg?branch=master) [![Build Status](https://travis-ci.org/bmatcuk/doublestar.svg?branch=master)](https://travis-ci.org/bmatcuk/doublestar) [![codecov.io](https://img.shields.io/codecov/c/github/bmatcuk/doublestar.svg?branch=master)](https://codecov.io/github/bmatcuk/doublestar?branch=master) # doublestar **doublestar** is a [golang](http://golang.org/) implementation of path pattern matching and globbing with support for "doublestar" (aka globstar: `**`) patterns. doublestar patterns match files and directories recursively. For example, if you had the following directory structure: ``` grandparent `-- parent |-- child1 `-- child2 ``` You could find the children with patterns such as: `**/child*`, `grandparent/**/child?`, `**/parent/*`, or even just `**` by itself (which will return all files and directories recursively). Bash's globstar is doublestar's inspiration and, as such, works similarly. Note that the doublestar must appear as a path component by itself. A pattern such as `/path**` is invalid and will be treated the same as `/path*`, but `/path*/**` should achieve the desired result. Additionally, `/path/**` will match all directories and files under the path directory, but `/path/**/` will only match directories. ## Installation **doublestar** can be installed via `go get`: ```bash go get github.com/bmatcuk/doublestar ``` To use it in your code, you must import it: ```go import "github.com/bmatcuk/doublestar" ``` ## Functions ### Match ```go func Match(pattern, name string) (bool, error) ``` Match returns true if `name` matches the file name `pattern` ([see below](#patterns)). `name` and `pattern` are split on forward slash (`/`) characters and may be relative or absolute. Note: `Match()` is meant to be a drop-in replacement for `path.Match()`. As such, it always uses `/` as the path separator. If you are writing code that will run on systems where `/` is not the path separator (such as Windows), you want to use `PathMatch()` (below) instead. ### PathMatch ```go func PathMatch(pattern, name string) (bool, error) ``` PathMatch returns true if `name` matches the file name `pattern` ([see below](#patterns)). The difference between Match and PathMatch is that PathMatch will automatically use your system's path separator to split `name` and `pattern`. `PathMatch()` is meant to be a drop-in replacement for `filepath.Match()`. ### Glob ```go func Glob(pattern string) ([]string, error) ``` Glob finds all files and directories in the filesystem that match `pattern` ([see below](#patterns)). `pattern` may be relative (to the current working directory), or absolute. `Glob()` is meant to be a drop-in replacement for `filepath.Glob()`. ## Patterns **doublestar** supports the following special terms in the patterns: Special Terms | Meaning ------------- | ------- `*` | matches any sequence of non-path-separators `**` | matches any sequence of characters, including path separators `?` | matches any single non-path-separator character `[class]` | matches any single non-path-separator character against a class of characters ([see below](#character-classes)) `{alt1,...}` | matches a sequence of characters if one of the comma-separated alternatives matches Any character with a special meaning can be escaped with a backslash (`\`). ### Character Classes Character classes support the following: Class | Meaning ---------- | ------- `[abc]` | matches any single character within the set `[a-z]` | matches any single character in the range `[^class]` | matches any single character which does *not* match the class doublestar-1.2.1/doublestar.go000066400000000000000000000417561357127513500164000ustar00rootroot00000000000000package doublestar import ( "fmt" "os" "path" "path/filepath" "strings" "unicode/utf8" ) // ErrBadPattern indicates a pattern was malformed. var ErrBadPattern = path.ErrBadPattern // Split a path on the given separator, respecting escaping. func splitPathOnSeparator(path string, separator rune) (ret []string) { idx := 0 if separator == '\\' { // if the separator is '\\', then we can just split... ret = strings.Split(path, string(separator)) idx = len(ret) } else { // otherwise, we need to be careful of situations where the separator was escaped cnt := strings.Count(path, string(separator)) if cnt == 0 { return []string{path} } ret = make([]string, cnt+1) pathlen := len(path) separatorLen := utf8.RuneLen(separator) emptyEnd := false for start := 0; start < pathlen; { end := indexRuneWithEscaping(path[start:], separator) if end == -1 { emptyEnd = false end = pathlen } else { emptyEnd = true end += start } ret[idx] = path[start:end] start = end + separatorLen idx++ } // If the last rune is a path separator, we need to append an empty string to // represent the last, empty path component. By default, the strings from // make([]string, ...) will be empty, so we just need to icrement the count if emptyEnd { idx++ } } return ret[:idx] } // Find the first index of a rune in a string, // ignoring any times the rune is escaped using "\". func indexRuneWithEscaping(s string, r rune) int { end := strings.IndexRune(s, r) if end == -1 { return -1 } if end > 0 && s[end-1] == '\\' { start := end + utf8.RuneLen(r) end = indexRuneWithEscaping(s[start:], r) if end != -1 { end += start } } 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: // // pattern: // { term } // term: // '*' matches any sequence of non-path-separators // '**' matches any sequence of characters, including // path separators. // '?' matches any single non-path-separator character // '[' [ '^' ] { character-range } ']' // character class (must be non-empty) // '{' { term } [ ',' { term } ... ] '}' // c matches character c (c != '*', '?', '\\', '[') // '\\' c matches character c // // character-range: // c matches character c (c != '\\', '-', ']') // '\\' c matches character c // lo '-' hi matches character c for lo <= c <= hi // // Match requires pattern to match all of name, not just a substring. // The path-separator defaults to the '/' character. The only possible // returned error is ErrBadPattern, when pattern is malformed. // // Note: this is meant as a drop-in replacement for path.Match() which // always uses '/' as the path separator. If you want to support systems // which use a different path separator (such as Windows), what you want // is the PathMatch() function below. // func Match(pattern, name string) (bool, error) { return matchWithSeparator(pattern, name, '/') } // PathMatch is like Match except that it uses your system's path separator. // For most systems, this will be '/'. However, for Windows, it would be '\\'. // Note that for systems where the path separator is '\\', escaping is // disabled. // // 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) } // Match returns true if name matches the shell file name pattern. // The pattern syntax is: // // pattern: // { term } // term: // '*' matches any sequence of non-path-separators // '**' matches any sequence of characters, including // path separators. // '?' matches any single non-path-separator character // '[' [ '^' ] { character-range } ']' // character class (must be non-empty) // '{' { term } [ ',' { term } ... ] '}' // c matches character c (c != '*', '?', '\\', '[') // '\\' c matches character c // // character-range: // c matches character c (c != '\\', '-', ']') // '\\' c matches character c, unless separator is '\\' // lo '-' hi matches character c for lo <= c <= hi // // Match requires pattern to match all of name, not just a substring. // The only possible returned error is ErrBadPattern, when pattern // is malformed. // func matchWithSeparator(pattern, name string, separator rune) (bool, error) { nameComponents := splitPathOnSeparator(name, separator) return doMatching(pattern, nameComponents) } func doMatching(pattern string, nameComponents []string) (matched bool, err error) { // check for some base-cases patternLen, nameLen := len(pattern), len(nameComponents) if patternLen == 0 && nameLen == 0 { return true, nil } if patternLen == 0 || nameLen == 0 { return false, 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 := 0; nameIdx < nameLen; nameIdx++ { if m, _ := doMatching(pattern[slashIdx+1:], nameComponents[nameIdx:]); m { return true, nil } } return false, nil } var matches []string matches, err = matchComponent(pattern, nameComponents[0]) if matches == nil || err != nil { return } if len(matches) == 0 && nameLen == 1 { return true, nil } if nameLen > 1 { for _, alt := range matches { matched, err = doMatching(alt, nameComponents[1:]) if matched || err != nil { return } } } return false, nil } // Glob returns the names of all files matching pattern or nil // if there is no matching file. The syntax of pattern is the same // as in Match. The pattern may describe hierarchical names such as // /usr/*/bin/ed (assuming the Separator is '/'). // // Glob ignores file system errors such as I/O errors reading directories. // The only possible returned error is ErrBadPattern, when pattern // is malformed. // // Your system path separator is automatically used. This means on // systems where the separator is '\\' (Windows), escaping will be // disabled. // // Note: this is meant as a drop-in replacement for filepath.Glob(). // func Glob(pattern string) (matches []string, err error) { if len(pattern) == 0 { return nil, nil } // if the pattern starts with alternatives, we need to handle that here - the // alternatives may be a mix of relative and absolute if pattern[0] == '{' { options, endOptions := splitAlternatives(pattern[1:]) if endOptions == -1 { return nil, ErrBadPattern } for _, o := range options { m, e := Glob(o + pattern[endOptions+1:]) if e != nil { return nil, e } matches = append(matches, m...) } return matches, nil } // 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(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(".", filepath.ToSlash(pattern), matches) } // Perform a glob func doGlob(basedir, pattern string, matches []string) (m []string, e error) { m = matches e = nil // 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 globIdx > 0 { basedir = filepath.Join(basedir, pattern[:globIdx]) pattern = pattern[globIdx+1:] } // Lstat will return an error if the file/directory doesn't exist fi, err := os.Lstat(basedir) if err != nil { return } // if the pattern is empty, we've found a match if len(pattern) == 0 { m = append(m, basedir) return } // otherwise, we need to check each item in the directory... // first, if basedir is a symlink, follow it... if (fi.Mode() & os.ModeSymlink) != 0 { fi, err = os.Stat(basedir) if err != nil { return } } // confirm it's a directory... if !fi.IsDir() { return } // read directory dir, err := os.Open(basedir) if err != nil { return } defer dir.Close() files, _ := dir.Readdir(-1) 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 if (file.Mode() & os.ModeSymlink) != 0 { file, err = os.Stat(filepath.Join(basedir, file.Name())) if err != nil { continue } } if file.IsDir() { // recurse into directories if lastComponent { m = append(m, filepath.Join(basedir, file.Name())) } 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())) } } if lastComponent { return // we're done } pattern = pattern[slashIdx+1:] } // check items in current directory and recurse var match []string for _, file := range files { match, e = matchComponent(pattern, file.Name()) if e != nil { return } if match != nil { if len(match) == 0 { m = append(m, filepath.Join(basedir, file.Name())) } else { for _, alt := range match { m, e = doGlob(filepath.Join(basedir, file.Name()), alt, m) } } } } return } // 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 == '/' { 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 nil, ErrBadPattern } else if patRune == nameRune { patIdx += patAdj nameIdx += nameAdj } else { return nil, nil } } else if patRune == '*' { // 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 { 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, e := matchComponent(pattern[patIdx:], name[nameIdx:]); m != nil || e != nil { return m, e } } return nil, nil } else if patRune == '[' { // handle character sets patIdx += patAdj endClass := indexRuneWithEscaping(pattern[patIdx:], ']') if endClass == -1 { return nil, ErrBadPattern } endClass += patIdx classRunes := []rune(pattern[patIdx:endClass]) classRunesLen := len(classRunes) if classRunesLen > 0 { classIdx := 0 matchClass := false if classRunes[0] == '^' { classIdx++ } for classIdx < classRunesLen { low := classRunes[classIdx] if low == '-' { return nil, ErrBadPattern } classIdx++ if low == '\\' { if classIdx < classRunesLen { low = classRunes[classIdx] classIdx++ } else { return nil, ErrBadPattern } } high := low if classIdx < classRunesLen && classRunes[classIdx] == '-' { // we have a range of runes if classIdx++; classIdx >= classRunesLen { return nil, ErrBadPattern } high = classRunes[classIdx] if high == '-' { return nil, ErrBadPattern } classIdx++ if high == '\\' { if classIdx < classRunesLen { high = classRunes[classIdx] classIdx++ } else { return nil, ErrBadPattern } } } if low <= nameRune && nameRune <= high { matchClass = true } } if matchClass == (classRunes[0] == '^') { return nil, nil } } else { return nil, ErrBadPattern } patIdx = endClass + 1 nameIdx += nameAdj } else if patRune == '{' { // handle alternatives such as {alt1,alt2,...} patIdx += patAdj options, endOptions := splitAlternatives(pattern[patIdx:]) if endOptions == -1 { return nil, ErrBadPattern } 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 nil, e } if m != nil { results = append(results, m) totalResults += len(m) } } if len(results) > 0 { lst := make([]string, 0, totalResults) for _, m := range results { lst = append(lst, m...) } return lst, nil } return nil, nil } else if patRune == '?' || patRune == nameRune { // handle single-rune wildcard patIdx += patAdj nameIdx += nameAdj } else { return nil, 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 nil, nil } doublestar-1.2.1/doublestar_test.go000066400000000000000000000251341357127513500174270ustar00rootroot00000000000000// This file is mostly copied from Go's path/match_test.go package doublestar import ( "log" "os" "path" "path/filepath" "runtime" "strings" "testing" ) 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 } // Tests which contain escapes and symlinks will not work on Windows var onWindows = runtime.GOOS == "windows" var matchTests = []MatchTest{ {"*", "", 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}, {"{a/ab*}", "a/abc", true, nil, true}, {"{a/*}", "a/b", true, nil, true}, {"{a/abc}", "a/abc", true, nil, true}, {"{a/b,a/c}", "a/c", true, 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) { for idx, tt := range matchTests { // Since Match() always uses "/" as the separator, we // don't need to worry about the tt.testOnDisk flag testMatchWith(t, idx, tt) } } func testMatchWith(t *testing.T, idx int, tt MatchTest) { defer func() { if r := recover(); r != nil { t.Errorf("#%v. Match(%#q, %#q) panicked: %#v", idx, tt.pattern, tt.testPath, r) } }() // Match() always uses "/" as the separator 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, tt.pattern, tt.testPath, ok, err, tt.shouldMatch, tt.expectedErr) } 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, tt.pattern, tt.testPath, ok, err, stdOk, stdErr) } } } func TestPathMatch(t *testing.T) { for idx, tt := range matchTests { // Even though we aren't actually matching paths on disk, we are using // PathMatch() which will use the system's separator. As a result, any // patterns that might cause problems on-disk need to also be avoided // here in this test. if tt.testOnDisk { testPathMatchWith(t, idx, tt) } } } func testPathMatchWith(t *testing.T, idx int, tt MatchTest) { defer func() { if r := recover(); r != nil { t.Errorf("#%v. Match(%#q, %#q) panicked: %#v", idx, tt.pattern, tt.testPath, r) } }() 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) } if isStandardPattern(pattern) { stdOk, stdErr := filepath.Match(pattern, testPath) if ok != stdOk || !compareErrors(err, stdErr) { t.Errorf("#%v. PathMatch(%#q, %#q) != filepath.Match(...). Got %v, %v want %v, %v", idx, pattern, testPath, ok, err, stdOk, stdErr) } } } func TestGlob(t *testing.T) { abspath, err := os.Getwd() if err != nil { t.Errorf("Error getting current working directory: %v", err) 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, "test") testGlobWith(t, idx, tt, abspath) } } } func testGlobWith(t *testing.T, idx int, tt MatchTest, basepath string) { defer func() { if r := recover(); r != nil { t.Errorf("#%v. Glob(%#q) panicked: %#v", idx, tt.pattern, r) } }() 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 { t.Errorf("#%v. Glob(%#q) = %#v - doesn't contain %v, but should", idx, pattern, matches, tt.testPath) } else { t.Errorf("#%v. Glob(%#q) = %#v - contains %v, but shouldn't", idx, pattern, matches, tt.testPath) } } if err != tt.expectedErr { t.Errorf("#%v. Glob(%#q) has error %v, but should be %v", idx, pattern, err, tt.expectedErr) } if isStandardPattern(pattern) { stdMatches, stdErr := filepath.Glob(pattern) if !compareSlices(matches, stdMatches) || !compareErrors(err, stdErr) { t.Errorf("#%v. Glob(%#q) != filepath.Glob(...). Got %#v, %v want %#v, %v", idx, pattern, matches, err, stdMatches, stdErr) } } } func joinWithoutClean(elem ...string) string { return strings.Join(elem, string(os.PathSeparator)) } func isStandardPattern(pattern string) bool { return !strings.Contains(pattern, "**") && indexRuneWithEscaping(pattern, '{') == -1 } func compareErrors(a, b error) bool { if a == nil { return b == nil } return b != nil } func inSlice(s string, a []string) bool { for _, i := range a { if i == s { return true } } return false } func compareSlices(a, b []string) bool { if len(a) != len(b) { return false } diff := make(map[string]int, len(a)) for _, x := range a { diff[x]++ } for _, y := range b { if _, ok := diff[y]; !ok { return false } diff[y]-- if diff[y] == 0 { delete(diff, y) } } return len(diff) == 0 } func mkdirp(parts ...string) { dirs := path.Join(parts...) err := os.MkdirAll(dirs, 0755) if err != nil { log.Fatalf("Could not create test directories %v: %v\n", dirs, err) } } func touch(parts ...string) { filename := path.Join(parts...) f, err := os.Create(filename) if err != nil { log.Fatalf("Could not create test file %v: %v\n", filename, err) } f.Close() } func symlink(oldname, newname string) { // since this will only run on non-windows, we can assume "/" as path separator err := os.Symlink(oldname, newname) if err != nil && !os.IsExist(err) { log.Fatalf("Could not create symlink %v -> %v: %v\n", oldname, newname, err) } } func TestMain(m *testing.M) { // create the test directory mkdirp("test", "a", "b", "c") mkdirp("test", "a", "c") mkdirp("test", "abc") mkdirp("test", "axbxcxdxe", "xxx") mkdirp("test", "axbxcxdxexxx") mkdirp("test", "b") // create test files touch("test", "a", "abc") touch("test", "a", "b", "c", "d") touch("test", "a", "c", "b") touch("test", "abc", "b") touch("test", "abcd") touch("test", "abcde") touch("test", "abxbbxdbxebxczzx") touch("test", "abxbbxdbxebxczzy") touch("test", "axbxcxdxe", "f") touch("test", "axbxcxdxe", "xxx", "f") touch("test", "axbxcxdxexxx", "f") touch("test", "axbxcxdxexxx", "fff") touch("test", "a☺b") touch("test", "b", "c") touch("test", "c") touch("test", "x") touch("test", "xxx") touch("test", "z") touch("test", "α") if !onWindows { // these files/symlinks won't work on Windows touch("test", "-") touch("test", "]") symlink("../axbxcxdxe/", "test/b/symlink-dir") symlink("/tmp/nonexistant-file-20160902155705", "test/broken-symlink") symlink("a/b", "test/working-symlink") } os.Exit(m.Run()) } doublestar-1.2.1/go.mod000066400000000000000000000000561357127513500147770ustar00rootroot00000000000000module github.com/bmatcuk/doublestar go 1.12