package doublestar import ( "fmt" "os" "path" "path/filepath" "strings" "unicode/utf8" ) var ErrBadPattern = path.ErrBadPattern // Split a path on the given separator, respecting escaping. func splitPathOnSeparator(path string, separator rune) []string { // if the separator is '\\', then we can just split... if separator == '\\' { return strings.Split(path, string(separator)) } // 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) idx := 0 for start := 0; start < pathlen; { end := indexRuneWithEscaping(path[start:], separator) if end == -1 { end = pathlen } else { end += start } ret[idx] = path[start:end] start = end + separatorLen 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 } // 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) { 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) { patternComponents := splitPathOnSeparator(pattern, separator) nameComponents := splitPathOnSeparator(name, separator) return doMatching(patternComponents, nameComponents) } func doMatching(patternComponents, nameComponents []string) (matched bool, err error) { // check for some base-cases patternLen, nameLen := len(patternComponents), len(nameComponents) if patternLen == 0 && nameLen == 0 { return true, nil } if patternLen == 0 || nameLen == 0 { 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 } // otherwise, try matching remaining components for ; nameIdx < nameLen; nameIdx++ { if m, _ := doMatching(patternComponents[patIdx:], nameComponents[nameIdx:]); m { return true, nil } } return false, nil } else { // try matching components matched, err = matchComponent(patternComponents[patIdx], nameComponents[nameIdx]) if !matched || err != nil { return } } patIdx++ nameIdx++ } return patIdx >= patternLen && nameIdx >= nameLen, 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) { patternComponents := splitPathOnSeparator(filepath.ToSlash(pattern), '/') if len(patternComponents) == 0 { return nil, nil } // On Windows systems, this will return the drive name ('C:'), on others, // it will return an empty string. volumeName := filepath.VolumeName(pattern) // If the first pattern component is equal to the volume name, then the // pattern is an absolute path. if patternComponents[0] == volumeName { return doGlob(fmt.Sprintf("%s%s", volumeName, string(os.PathSeparator)), patternComponents[1:], matches) } // otherwise, it's a relative pattern return doGlob(".", patternComponents, matches) } // Perform a glob func doGlob(basedir string, components, 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 patIdx > 0 { basedir = filepath.Join(basedir, filepath.Join(components[0:patIdx]...)) } // Lstat will return an error if the file/directory doesn't exist fi, err := os.Lstat(basedir) if err != nil { return } // if there are no more components, we've found a match if patIdx >= patLen { 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) lastComponent := (patIdx + 1) >= patLen if components[patIdx] == "**" { // 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()), components[patIdx:], 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 } patIdx++ lastComponent = (patIdx + 1) >= patLen } // check items in current directory and recurse var match bool for _, file := range files { match, e = matchComponent(components[patIdx], file.Name()) if e != nil { return } if match { if lastComponent { m = append(m, filepath.Join(basedir, file.Name())) } else { m, e = doGlob(filepath.Join(basedir, file.Name()), components[patIdx+1:], 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 } // check for matches one rune at a time 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 patIdx += patAdj patRune, patAdj = utf8.DecodeRuneInString(pattern[patIdx:]) if patRune == utf8.RuneError { return false, ErrBadPattern } else if patRune == nameRune { patIdx += patAdj nameIdx += nameAdj } else { return false, nil } } else if patRune == '*' { // handle stars if patIdx += patAdj; patIdx >= patternLen { // a star at the end of a pattern will always // match the rest of the path return true, nil } // check if we can make any matches for ; nameIdx < nameLen; nameIdx += nameAdj { if m, _ := matchComponent(pattern[patIdx:], name[nameIdx:]); m { return true, nil } } return false, nil } else if patRune == '[' { // handle character sets patIdx += patAdj endClass := indexRuneWithEscaping(pattern[patIdx:], ']') if endClass == -1 { return false, 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 false, ErrBadPattern } classIdx++ if low == '\\' { if classIdx < classRunesLen { low = classRunes[classIdx] classIdx++ } else { return false, ErrBadPattern } } high := low if classIdx < classRunesLen && classRunes[classIdx] == '-' { // we have a range of runes if classIdx++; classIdx >= classRunesLen { return false, ErrBadPattern } high = classRunes[classIdx] if high == '-' { return false, ErrBadPattern } classIdx++ if high == '\\' { if classIdx < classRunesLen { high = classRunes[classIdx] classIdx++ } else { return false, ErrBadPattern } } } if low <= nameRune && nameRune <= high { matchClass = true } } if matchClass == (classRunes[0] == '^') { return false, nil } } else { return false, ErrBadPattern } patIdx = endClass + 1 nameIdx += nameAdj } else if patRune == '{' { // handle alternatives such as {alt1,alt2,...} patIdx += patAdj endOptions := indexRuneWithEscaping(pattern[patIdx:], '}') if endOptions == -1 { return false, ErrBadPattern } endOptions += patIdx options := splitPathOnSeparator(pattern[patIdx:endOptions], ',') patIdx = endOptions + 1 for _, o := range options { m, e := matchComponent(o+pattern[patIdx:], name[nameIdx:]) if e != nil { return false, e } if m { return true, nil } } return false, nil } else if patRune == '?' || patRune == nameRune { // handle single-rune wildcard patIdx += patAdj nameIdx += nameAdj } else { return false, nil } } if patIdx >= patternLen && nameIdx >= nameLen { return true, nil } if nameIdx >= nameLen && pattern[patIdx:] == "*" || pattern[patIdx:] == "**" { return true, nil } return false, nil }