pax_global_header00006660000000000000000000000064137553365540014532gustar00rootroot0000000000000052 comment=a4f8dd29c928b08296169383e075001046a0cd43 doublestar-2.0.4/000077500000000000000000000000001375533655400137015ustar00rootroot00000000000000doublestar-2.0.4/.gitignore000066400000000000000000000004671375533655400157000ustar00rootroot00000000000000# 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-2.0.4/.travis.yml000066400000000000000000000003651375533655400160160ustar00rootroot00000000000000language: go go: - 1.12 - 1.13 - 1.14 os: - linux - windows 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-2.0.4/LICENSE000066400000000000000000000020661375533655400147120ustar00rootroot00000000000000The 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-2.0.4/README.md000066400000000000000000000116511375533655400151640ustar00rootroot00000000000000# doublestar Path pattern matching and globbing supporting `doublestar` (`**`) patterns. [![PkgGoDev](https://pkg.go.dev/badge/github.com/bmatcuk/doublestar)](https://pkg.go.dev/github.com/bmatcuk/doublestar/v2) [![Release](https://img.shields.io/github/release/bmatcuk/doublestar.svg?branch=master)](https://github.com/bmatcuk/doublestar/releases) [![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) ## About #### [Updating from v1 to v2?](UPGRADING.md) **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: ```bash 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/v2 ``` To use it in your code, you must import it: ```go import "github.com/bmatcuk/doublestar/v2" ``` ## Usage ### 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 (`\`). A mid-pattern doublestar (`**`) behaves like bash's globstar option: a pattern such as `path/to/**.txt` would return the same results as `path/to/*.txt`. The pattern you're looking for is `path/to/**/*.txt`. #### 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 ### Abstracting the `os` package **doublestar** by default uses the `Open`, `Stat`, and `Lstat`, functions and `PathSeparator` value from the standard library's `os` package. To abstract this, for example to be able to perform tests of Windows paths on Linux, or to interoperate with your own filesystem code, it includes the functions `GlobOS` and `PathMatchOS` which are identical to `Glob` and `PathMatch` except that they operate on an `OS` interface: ```go type OS interface { Lstat(name string) (os.FileInfo, error) Open(name string) (*os.File, error) PathSeparator() rune Stat(name string) (os.FileInfo, error) } ``` `StandardOS` is a value that implements this interface by calling functions in the standard library's `os` package. ## License [MIT License](LICENSE) doublestar-2.0.4/UPGRADING.md000066400000000000000000000013411375533655400155420ustar00rootroot00000000000000# Upgrading from v1 to v2 The change from v1 to v2 was fairly minor: the return type of the `Open` method on the `OS` interface was changed from `*os.File` to `File`, a new interface exported by doublestar. The new `File` interface only defines the functionality doublestar actually needs (`io.Closer` and `Readdir`), making it easier to use doublestar with [go-billy](https://github.com/src-d/go-billy), [afero](https://github.com/spf13/afero), or something similar. If you were using this functionality, updating should be as easy as updating `Open's` return type, since `os.File` already implements `doublestar.File`. If you weren't using this functionality, updating should be as easy as changing your dependencies to point to v2. doublestar-2.0.4/doublestar.go000066400000000000000000000413671375533655400164070ustar00rootroot00000000000000package doublestar import ( "fmt" "io" "os" "path" "path/filepath" "sort" "strings" "unicode/utf8" ) // File defines a subset of file operations type File interface { io.Closer Readdir(count int) ([]os.FileInfo, error) } // An OS abstracts functions in the standard library's os package. type OS interface { Lstat(name string) (os.FileInfo, error) Open(name string) (File, error) PathSeparator() rune Stat(name string) (os.FileInfo, error) } // A standardOS implements OS by calling functions in the standard library's os // package. type standardOS struct{} func (standardOS) Lstat(name string) (os.FileInfo, error) { return os.Lstat(name) } func (standardOS) Open(name string) (File, error) { return os.Open(name) } func (standardOS) PathSeparator() rune { return os.PathSeparator } func (standardOS) Stat(name string) (os.FileInfo, error) { return os.Stat(name) } // StandardOS is a value that implements the OS interface by calling functions // in the standard libray's os package. var StandardOS OS = standardOS{} // ErrBadPattern indicates a pattern was malformed. var ErrBadPattern = path.ErrBadPattern // 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 || r == '\\' { return end } 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 doMatching(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) { return PathMatchOS(StandardOS, pattern, name) } // PathMatchOS is like PathMatch except that it uses vos's path separator. func PathMatchOS(vos OS, pattern, name string) (bool, error) { pattern = filepath.ToSlash(pattern) return doMatching(pattern, name, vos.PathSeparator()) } func doMatching(pattern, name string, separator rune) (matched bool, err error) { // check for some base-cases patternLen, nameLen := len(pattern), len(name) if patternLen == 0 { return nameLen == 0, nil } else if nameLen == 0 { return isZeroLengthPattern(pattern) } separatorAdj := utf8.RuneLen(separator) patIdx := indexRuneWithEscaping(pattern, '/') lastPat := patIdx == -1 if lastPat { patIdx = len(pattern) } if pattern[:patIdx] == "**" { // if our last pattern component is a doublestar, we're done - // doublestar will match any remaining name components, if any. if lastPat { return true, nil } // otherwise, try matching remaining components nameIdx := 0 patIdx += 1 for { if m, _ := doMatching(pattern[patIdx:], name[nameIdx:], separator); m { return true, nil } nextNameIdx := 0 if nextNameIdx = indexRuneWithEscaping(name[nameIdx:], separator); nextNameIdx == -1 { break } nameIdx += separatorAdj + nextNameIdx } return false, nil } nameIdx := indexRuneWithEscaping(name, separator) lastName := nameIdx == -1 if lastName { nameIdx = nameLen } var matches []string matches, err = matchComponent(pattern, name[:nameIdx]) if matches == nil || err != nil { return } if len(matches) == 0 && lastName { return true, nil } if !lastName { nameIdx += separatorAdj for _, alt := range matches { matched, err = doMatching(alt, name[nameIdx:], separator) 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) { return GlobOS(StandardOS, pattern) } // GlobOS is like Glob except that it operates on vos. func GlobOS(vos OS, 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 := GlobOS(vos, 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) || pattern[0] == '\\' || pattern[0] == '/' volumeName := filepath.VolumeName(pattern) isWindowsUNC := strings.HasPrefix(volumeName, `\\`) if isWindowsUNC || isAbs { startIdx := len(volumeName) + 1 return doGlob(vos, fmt.Sprintf("%s%s", volumeName, string(vos.PathSeparator())), filepath.ToSlash(pattern[startIdx:]), matches) } // otherwise, it's a relative pattern return doGlob(vos, ".", filepath.ToSlash(pattern), matches) } // Perform a glob func doGlob(vos OS, 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 := vos.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 = vos.Stat(basedir) if err != nil { return } } // confirm it's a directory... if !fi.IsDir() { return } files, err := filesInDir(vos, basedir) if err != nil { return } sort.Slice(files, func(i, j int) bool { return files[i].Name() < files[j].Name() }) 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 = vos.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(vos, 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(vos, filepath.Join(basedir, file.Name()), alt, m) } } } } return } func filesInDir(vos OS, dirPath string) (files []os.FileInfo, e error) { dir, err := vos.Open(dirPath) if err != nil { return nil, nil } defer func() { if err := dir.Close(); e == nil { e = err } }() files, err = dir.Readdir(-1) if err != nil { return nil, nil } 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 } _, nameAdj = utf8.DecodeRuneInString(name[nameIdx:]) } 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-2.0.4/doublestar_test.go000066400000000000000000000332511375533655400174370ustar00rootroot00000000000000// 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 isStandard bool // pattern doesn't use any doublestar features 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, true, false}, {"*", "/", false, nil, true, false}, {"/*", "/", true, nil, true, false}, {"/*", "/debug/", false, nil, true, false}, {"/*", "//", false, nil, true, false}, {"abc", "abc", true, nil, true, true}, {"*", "abc", true, nil, true, true}, {"*c", "abc", true, nil, true, true}, {"*/", "a/", true, nil, true, false}, {"a*", "a", true, nil, true, true}, {"a*", "abc", true, nil, true, true}, {"a*", "ab/c", false, nil, true, true}, {"a*/b", "abc/b", true, nil, true, true}, {"a*/b", "a/c/b", false, nil, true, true}, {"a*b*c*d*e*", "axbxcxdxe", true, nil, true, true}, {"a*b*c*d*e*/f", "axbxcxdxe/f", true, nil, true, true}, {"a*b*c*d*e*/f", "axbxcxdxexxx/f", true, nil, true, true}, {"a*b*c*d*e*/f", "axbxcxdxe/xxx/f", false, nil, true, true}, {"a*b*c*d*e*/f", "axbxcxdxexxx/fff", false, nil, true, true}, {"a*b?c*x", "abxbbxdbxebxczzx", true, nil, true, true}, {"a*b?c*x", "abxbbxdbxebxczzy", false, nil, true, true}, {"ab[c]", "abc", true, nil, true, true}, {"ab[b-d]", "abc", true, nil, true, true}, {"ab[e-g]", "abc", false, nil, true, true}, {"ab[^c]", "abc", false, nil, true, true}, {"ab[^b-d]", "abc", false, nil, true, true}, {"ab[^e-g]", "abc", true, nil, true, true}, {"a\\*b", "ab", false, nil, true, true}, {"a?b", "a☺b", true, nil, true, true}, {"a[^a]b", "a☺b", true, nil, true, true}, {"a???b", "a☺b", false, nil, true, true}, {"a[^a][^a][^a]b", "a☺b", false, nil, true, true}, {"[a-ζ]*", "α", true, nil, true, true}, {"*[a-ζ]", "A", false, nil, true, true}, {"a?b", "a/b", false, nil, true, true}, {"a*b", "a/b", false, nil, true, true}, {"[\\]a]", "]", true, nil, true, !onWindows}, {"[\\-]", "-", true, nil, true, !onWindows}, {"[x\\-]", "x", true, nil, true, !onWindows}, {"[x\\-]", "-", true, nil, true, !onWindows}, {"[x\\-]", "z", false, nil, true, !onWindows}, {"[\\-x]", "x", true, nil, true, !onWindows}, {"[\\-x]", "-", true, nil, true, !onWindows}, {"[\\-x]", "a", false, nil, true, !onWindows}, {"[]a]", "]", false, ErrBadPattern, true, true}, {"[-]", "-", false, ErrBadPattern, true, true}, {"[x-]", "x", false, ErrBadPattern, true, true}, {"[x-]", "-", false, ErrBadPattern, true, true}, {"[x-]", "z", false, ErrBadPattern, true, true}, {"[-x]", "x", false, ErrBadPattern, true, true}, {"[-x]", "-", false, ErrBadPattern, true, true}, {"[-x]", "a", false, ErrBadPattern, true, true}, {"\\", "a", false, ErrBadPattern, true, !onWindows}, {"[a-b-c]", "a", false, ErrBadPattern, true, true}, {"[", "a", false, ErrBadPattern, true, true}, {"[^", "a", false, ErrBadPattern, true, true}, {"[^bc", "a", false, ErrBadPattern, true, true}, {"a[", "a", false, nil, true, false}, {"a[", "ab", false, ErrBadPattern, true, true}, {"*x", "xxx", true, nil, true, true}, {"[abc]", "b", true, nil, true, true}, {"**", "", true, nil, false, false}, {"a/**", "a", false, nil, false, true}, {"a/**", "a/b", true, nil, false, true}, {"a/**", "a/b/c", true, nil, false, true}, {"**/c", "c", true, nil, false, true}, {"**/c", "b/c", true, nil, false, true}, {"**/c", "a/b/c", true, nil, false, true}, {"**/c", "a/b", false, nil, false, true}, {"**/c", "abcd", false, nil, false, true}, {"**/c", "a/abc", false, nil, false, true}, {"a/**/b", "a/b", true, nil, false, true}, {"a/**/c", "a/b/c", true, nil, false, true}, {"a/**/d", "a/b/c/d", true, nil, false, true}, {"a/\\**", "a/b/c", false, nil, false, !onWindows}, // this is an odd case: filepath.Glob() will return results {"a//b/c", "a/b/c", false, nil, true, false}, {"a/b/c", "a/b//c", false, nil, true, true}, // also odd: Glob + filepath.Glob return results {"a/", "a", false, nil, true, false}, {"ab{c,d}", "abc", true, nil, false, true}, {"ab{c,d,*}", "abcde", true, nil, false, true}, {"ab{c,d}[", "abcd", false, ErrBadPattern, false, true}, {"a{,bc}", "a", true, nil, false, true}, {"a{,bc}", "abc", true, nil, false, true}, {"a/{b/c,c/b}", "a/b/c", true, nil, false, true}, {"a/{b/c,c/b}", "a/c/b", true, nil, false, true}, {"{a/{b,c},abc}", "a/b", true, nil, false, true}, {"{a/{b,c},abc}", "a/c", true, nil, false, true}, {"{a/{b,c},abc}", "abc", true, nil, false, true}, {"{a/{b,c},abc}", "a/b/c", false, nil, false, true}, {"{a/ab*}", "a/abc", true, nil, false, true}, {"{a/*}", "a/b", true, nil, false, true}, {"{a/abc}", "a/abc", true, nil, false, true}, {"{a/b,a/c}", "a/c", true, nil, false, true}, {"abc/**", "abc/b", true, nil, false, true}, {"**/abc", "abc", true, nil, false, true}, {"abc**", "abc/b", false, nil, false, true}, {"**/*.txt", "abc/【test】.txt", true, nil, false, true}, {"**/【*", "abc/【test】.txt", true, nil, false, true}, {"broken-symlink", "broken-symlink", true, nil, true, !onWindows}, {"working-symlink/c/*", "working-symlink/c/d", true, nil, true, !onWindows}, {"working-sym*/*", "working-symlink/c", true, nil, true, !onWindows}, {"b/**/f", "b/symlink-dir/f", true, nil, false, !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 tt.isStandard { 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 BenchmarkMatch(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { for _, tt := range matchTests { if tt.isStandard { Match(tt.pattern, tt.testPath) } } } } func BenchmarkGoMatch(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { for _, tt := range matchTests { if tt.isStandard { path.Match(tt.pattern, tt.testPath) } } } } 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 tt.isStandard { 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 BenchmarkPathMatch(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { for _, tt := range matchTests { if tt.isStandard && tt.testOnDisk { pattern := filepath.FromSlash(tt.pattern) testPath := filepath.FromSlash(tt.testPath) PathMatch(pattern, testPath) } } } } func BenchmarkGoPathMatch(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { for _, tt := range matchTests { if tt.isStandard && tt.testOnDisk { pattern := filepath.FromSlash(tt.pattern) testPath := filepath.FromSlash(tt.testPath) filepath.Match(pattern, testPath) } } } } 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") abspathWithoutVolume := "" volumeName := filepath.VolumeName(abspath) if volumeName != "" && !strings.HasPrefix(volumeName, `\\`) { abspathWithoutVolume = strings.TrimPrefix(abspath, volumeName) } 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) if abspathWithoutVolume != "" { testGlobWith(t, idx, tt, abspathWithoutVolume) } } } } 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 tt.isStandard { 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 BenchmarkGlob(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { for _, tt := range matchTests { if tt.isStandard && tt.testOnDisk { pattern := joinWithoutClean("test", filepath.FromSlash(tt.pattern)) Glob(pattern) } } } } func BenchmarkGoGlob(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { for _, tt := range matchTests { if tt.isStandard && tt.testOnDisk { pattern := joinWithoutClean("test", filepath.FromSlash(tt.pattern)) filepath.Glob(pattern) } } } } func joinWithoutClean(elem ...string) string { return strings.Join(elem, string(os.PathSeparator)) } 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 TestGlobSorted(t *testing.T) { expected := []string{"a", "abc", "abcd", "abcde", "abxbbxdbxebxczzx", "abxbbxdbxebxczzy", "axbxcxdxe", "axbxcxdxexxx", "a☺b"} matches, err := Glob(joinWithoutClean("test", "a*")) if err != nil { t.Errorf("Unexpected error %v", err) return } if len(matches) != len(expected) { t.Errorf("Glob returned %#v; expected %#v", matches, expected) return } for idx, match := range matches { if match != joinWithoutClean("test", expected[idx]) { t.Errorf("Glob returned %#v; expected %#v", matches, expected) return } } } 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", "α") touch("test", "abc", "【test】.txt") 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-2.0.4/examples/000077500000000000000000000000001375533655400155175ustar00rootroot00000000000000doublestar-2.0.4/examples/find.go000066400000000000000000000010511375533655400167630ustar00rootroot00000000000000package main import ( "fmt" "os" "strings" "github.com/bmatcuk/doublestar/v2" ) // To run: // $ go run find.go // For example: // $ go run find.go '/usr/bin/*' # Make sure to escape as necessary for your shell func main() { pattern := os.Args[1] fmt.Printf("Searching on disk for pattern: %s\n\n", pattern) matches, err := doublestar.Glob(pattern) if err != nil { fmt.Printf("Error: %v", err) os.Exit(1) } fmt.Printf(strings.Join(matches, "\n")) fmt.Print("\n\n") fmt.Printf("Found %d items.\n", len(matches)) } doublestar-2.0.4/go.mod000066400000000000000000000000611375533655400150040ustar00rootroot00000000000000module github.com/bmatcuk/doublestar/v2 go 1.12