| package 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 \\<server>\<share> 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 |
| } |