| package doublestar |
| |
| import ( |
| "errors" |
| "io/fs" |
| "path" |
| "path/filepath" |
| "strings" |
| ) |
| |
| // If returned from GlobWalkFunc, will cause GlobWalk to skip the current |
| // directory. In other words, if the current path is a directory, GlobWalk will |
| // not recurse into it. Otherwise, GlobWalk will skip the rest of the current |
| // directory. |
| var SkipDir = fs.SkipDir |
| |
| // Callback function for GlobWalk(). If the function returns an error, GlobWalk |
| // will end immediately and return the same error. |
| type GlobWalkFunc func(path string, d fs.DirEntry) error |
| |
| // GlobWalk calls the callback function `fn` for every file matching pattern. |
| // The syntax of pattern is the same as in Match() and the behavior is the same |
| // as Glob(), with regard to limitations (such as patterns containing `/./`, |
| // `/../`, or starting with `/`). The pattern may describe hierarchical names |
| // such as usr/*/bin/ed. |
| // |
| // GlobWalk may have a small performance benefit over Glob if you do not need a |
| // slice of matches because it can avoid allocating memory for the matches. |
| // Additionally, GlobWalk gives you access to the `fs.DirEntry` objects for |
| // each match, and lets you quit early by returning a non-nil error from your |
| // callback function. Like `io/fs.WalkDir`, if your callback returns `SkipDir`, |
| // GlobWalk will skip the current directory. This means that if the current |
| // path _is_ a directory, GlobWalk will not recurse into it. If the current |
| // path is not a directory, the rest of the parent directory will be skipped. |
| // |
| // GlobWalk ignores file system errors such as I/O errors reading directories |
| // by default. GlobWalk may return ErrBadPattern, reporting that the pattern is |
| // malformed. |
| // |
| // To enable aborting on I/O errors, the WithFailOnIOErrors option can be |
| // passed. |
| // |
| // Additionally, if the callback function `fn` returns an error, GlobWalk will |
| // exit immediately and return that error. |
| // |
| // Like Glob(), this function assumes that your pattern uses `/` as the path |
| // separator even if that's not correct for your OS (like Windows). If you |
| // aren't sure if that's the case, you can use filepath.ToSlash() on your |
| // pattern before calling GlobWalk(). |
| // |
| // Note: users should _not_ count on the returned error, |
| // doublestar.ErrBadPattern, being equal to path.ErrBadPattern. |
| // |
| func GlobWalk(fsys fs.FS, pattern string, fn GlobWalkFunc, opts ...GlobOption) error { |
| if !ValidatePattern(pattern) { |
| return ErrBadPattern |
| } |
| |
| g := newGlob(opts...) |
| return g.doGlobWalk(fsys, pattern, true, true, fn) |
| } |
| |
| // Actually execute GlobWalk |
| // - firstSegment is true if we're in the first segment of the pattern, ie, |
| // the right-most part where we can match files. If it's false, we're |
| // somewhere in the middle (or at the beginning) and can only match |
| // directories since there are path segments above us. |
| // - beforeMeta is true if we're exploring segments before any meta |
| // characters, ie, in a pattern such as `path/to/file*.txt`, the `path/to/` |
| // bit does not contain any meta characters. |
| func (g *glob) doGlobWalk(fsys fs.FS, pattern string, firstSegment, beforeMeta bool, fn GlobWalkFunc) error { |
| patternStart := indexMeta(pattern) |
| if patternStart == -1 { |
| // pattern doesn't contain any meta characters - does a file matching the |
| // pattern exist? |
| // The pattern may contain escaped wildcard characters for an exact path match. |
| path := unescapeMeta(pattern) |
| info, pathExists, err := g.exists(fsys, path, beforeMeta) |
| if pathExists && (!firstSegment || !g.filesOnly || !info.IsDir()) { |
| err = fn(path, dirEntryFromFileInfo(info)) |
| if err == SkipDir { |
| err = nil |
| } |
| } |
| return err |
| } |
| |
| dir := "." |
| splitIdx := lastIndexSlashOrAlt(pattern) |
| if splitIdx != -1 { |
| if pattern[splitIdx] == '}' { |
| openingIdx := indexMatchedOpeningAlt(pattern[:splitIdx]) |
| if openingIdx == -1 { |
| // if there's no matching opening index, technically Match() will treat |
| // an unmatched `}` as nothing special, so... we will, too! |
| splitIdx = lastIndexSlash(pattern[:splitIdx]) |
| if splitIdx != -1 { |
| dir = pattern[:splitIdx] |
| pattern = pattern[splitIdx+1:] |
| } |
| } else { |
| // otherwise, we have to handle the alts: |
| return g.globAltsWalk(fsys, pattern, openingIdx, splitIdx, firstSegment, beforeMeta, fn) |
| } |
| } else { |
| dir = pattern[:splitIdx] |
| pattern = pattern[splitIdx+1:] |
| } |
| } |
| |
| // if `splitIdx` is less than `patternStart`, we know `dir` has no meta |
| // characters. They would be equal if they are both -1, which means `dir` |
| // will be ".", and we know that doesn't have meta characters either. |
| if splitIdx <= patternStart { |
| return g.globDirWalk(fsys, dir, pattern, firstSegment, beforeMeta, fn) |
| } |
| |
| return g.doGlobWalk(fsys, dir, false, beforeMeta, func(p string, d fs.DirEntry) error { |
| if err := g.globDirWalk(fsys, p, pattern, firstSegment, false, fn); err != nil { |
| return err |
| } |
| return nil |
| }) |
| } |
| |
| // handle alts in the glob pattern - `openingIdx` and `closingIdx` are the |
| // indexes of `{` and `}`, respectively |
| func (g *glob) globAltsWalk(fsys fs.FS, pattern string, openingIdx, closingIdx int, firstSegment, beforeMeta bool, fn GlobWalkFunc) (err error) { |
| var matches []DirEntryWithFullPath |
| startIdx := 0 |
| afterIdx := closingIdx + 1 |
| splitIdx := lastIndexSlashOrAlt(pattern[:openingIdx]) |
| if splitIdx == -1 || pattern[splitIdx] == '}' { |
| // no common prefix |
| matches, err = g.doGlobAltsWalk(fsys, "", pattern, startIdx, openingIdx, closingIdx, afterIdx, firstSegment, beforeMeta, matches) |
| if err != nil { |
| return |
| } |
| } else { |
| // our alts have a common prefix that we can process first |
| startIdx = splitIdx + 1 |
| innerBeforeMeta := beforeMeta && !hasMetaExceptAlts(pattern[:splitIdx]) |
| err = g.doGlobWalk(fsys, pattern[:splitIdx], false, beforeMeta, func(p string, d fs.DirEntry) (e error) { |
| matches, e = g.doGlobAltsWalk(fsys, p, pattern, startIdx, openingIdx, closingIdx, afterIdx, firstSegment, innerBeforeMeta, matches) |
| return e |
| }) |
| if err != nil { |
| return |
| } |
| } |
| |
| skip := "" |
| for _, m := range matches { |
| if skip != "" { |
| // Because matches are sorted, we know that descendants of the skipped |
| // item must come immediately after the skipped item. If we find an item |
| // that does not have a prefix matching the skipped item, we know we're |
| // done skipping. I'm using strings.HasPrefix here because |
| // filepath.HasPrefix has been marked deprecated (and just calls |
| // strings.HasPrefix anyway). The reason it's deprecated is because it |
| // doesn't handle case-insensitive paths, nor does it guarantee that the |
| // prefix is actually a parent directory. Neither is an issue here: the |
| // paths come from the system so their cases will match, and we guarantee |
| // a parent directory by appending a slash to the prefix. |
| // |
| // NOTE: m.Path will always use slashes as path separators. |
| if strings.HasPrefix(m.Path, skip) { |
| continue |
| } |
| skip = "" |
| } |
| if err = fn(m.Path, m.Entry); err != nil { |
| if err == SkipDir { |
| isDir, err := g.isDir(fsys, "", m.Path, m.Entry) |
| if err != nil { |
| return err |
| } |
| if isDir { |
| // append a slash to guarantee `skip` will be treated as a parent dir |
| skip = m.Path + "/" |
| } else { |
| // Dir() calls Clean() which calls FromSlash(), so we need to convert |
| // back to slashes |
| skip = filepath.ToSlash(filepath.Dir(m.Path)) + "/" |
| } |
| err = nil |
| continue |
| } |
| return |
| } |
| } |
| |
| return |
| } |
| |
| // runs actual matching for alts |
| func (g *glob) doGlobAltsWalk(fsys fs.FS, d, pattern string, startIdx, openingIdx, closingIdx, afterIdx int, firstSegment, beforeMeta bool, m []DirEntryWithFullPath) (matches []DirEntryWithFullPath, err error) { |
| matches = m |
| matchesLen := len(m) |
| patIdx := openingIdx + 1 |
| for patIdx < closingIdx { |
| nextIdx := indexNextAlt(pattern[patIdx:closingIdx], true) |
| if nextIdx == -1 { |
| nextIdx = closingIdx |
| } else { |
| nextIdx += patIdx |
| } |
| |
| alt := buildAlt(d, pattern, startIdx, openingIdx, patIdx, nextIdx, afterIdx) |
| err = g.doGlobWalk(fsys, alt, firstSegment, beforeMeta, func(p string, d fs.DirEntry) error { |
| // insertion sort, ignoring dups |
| insertIdx := matchesLen |
| for insertIdx > 0 && matches[insertIdx-1].Path > p { |
| insertIdx-- |
| } |
| if insertIdx > 0 && matches[insertIdx-1].Path == p { |
| // dup |
| return nil |
| } |
| |
| // append to grow the slice, then insert |
| entry := DirEntryWithFullPath{d, p} |
| matches = append(matches, entry) |
| for i := matchesLen; i > insertIdx; i-- { |
| matches[i] = matches[i-1] |
| } |
| matches[insertIdx] = entry |
| matchesLen++ |
| |
| return nil |
| }) |
| if err != nil { |
| return |
| } |
| |
| patIdx = nextIdx + 1 |
| } |
| |
| return |
| } |
| |
| func (g *glob) globDirWalk(fsys fs.FS, dir, pattern string, canMatchFiles, beforeMeta bool, fn GlobWalkFunc) (e error) { |
| if pattern == "" { |
| if !canMatchFiles || !g.filesOnly { |
| // pattern can be an empty string if the original pattern ended in a |
| // slash, in which case, we should just return dir, but only if it |
| // actually exists and it's a directory (or a symlink to a directory) |
| info, isDir, err := g.isPathDir(fsys, dir, beforeMeta) |
| if err != nil { |
| return err |
| } |
| if isDir { |
| e = fn(dir, dirEntryFromFileInfo(info)) |
| if e == SkipDir { |
| e = nil |
| } |
| } |
| } |
| return |
| } |
| |
| if pattern == "**" { |
| // `**` can match *this* dir |
| info, dirExists, err := g.exists(fsys, dir, beforeMeta) |
| if err != nil { |
| return err |
| } |
| if !dirExists || !info.IsDir() { |
| return nil |
| } |
| if !canMatchFiles || !g.filesOnly { |
| if e = fn(dir, dirEntryFromFileInfo(info)); e != nil { |
| if e == SkipDir { |
| e = nil |
| } |
| return |
| } |
| } |
| return g.globDoubleStarWalk(fsys, dir, canMatchFiles, fn) |
| } |
| |
| dirs, err := fs.ReadDir(fsys, dir) |
| if err != nil { |
| if errors.Is(err, fs.ErrNotExist) { |
| return g.handlePatternNotExist(beforeMeta) |
| } |
| return g.forwardErrIfFailOnIOErrors(err) |
| } |
| |
| var matched bool |
| for _, info := range dirs { |
| name := info.Name() |
| matched, e = matchWithSeparator(pattern, name, '/', false) |
| if e != nil { |
| return |
| } |
| if matched { |
| matched = canMatchFiles |
| if !matched || g.filesOnly { |
| matched, e = g.isDir(fsys, dir, name, info) |
| if e != nil { |
| return e |
| } |
| if canMatchFiles { |
| // if we're here, it's because g.filesOnly |
| // is set and we don't want directories |
| matched = !matched |
| } |
| } |
| if matched { |
| if e = fn(path.Join(dir, name), info); e != nil { |
| if e == SkipDir { |
| e = nil |
| } |
| return |
| } |
| } |
| } |
| } |
| |
| return |
| } |
| |
| // recursively walk files/directories in a directory |
| func (g *glob) globDoubleStarWalk(fsys fs.FS, dir string, canMatchFiles bool, fn GlobWalkFunc) (e error) { |
| dirs, err := fs.ReadDir(fsys, dir) |
| if err != nil { |
| if errors.Is(err, fs.ErrNotExist) { |
| // This function is only ever called after we know the top-most directory |
| // exists, so, if we ever get here, we know we'll never return |
| // ErrPatternNotExist. |
| return nil |
| } |
| return g.forwardErrIfFailOnIOErrors(err) |
| } |
| |
| for _, info := range dirs { |
| name := info.Name() |
| isDir, err := g.isDir(fsys, dir, name, info) |
| if err != nil { |
| return err |
| } |
| |
| if isDir { |
| p := path.Join(dir, name) |
| if !canMatchFiles || !g.filesOnly { |
| // `**` can match *this* dir, so add it |
| if e = fn(p, info); e != nil { |
| if e == SkipDir { |
| e = nil |
| continue |
| } |
| return |
| } |
| } |
| if e = g.globDoubleStarWalk(fsys, p, canMatchFiles, fn); e != nil { |
| return |
| } |
| } else if canMatchFiles { |
| if e = fn(path.Join(dir, name), info); e != nil { |
| if e == SkipDir { |
| e = nil |
| } |
| return |
| } |
| } |
| } |
| |
| return |
| } |
| |
| type DirEntryFromFileInfo struct { |
| fi fs.FileInfo |
| } |
| |
| func (d *DirEntryFromFileInfo) Name() string { |
| return d.fi.Name() |
| } |
| |
| func (d *DirEntryFromFileInfo) IsDir() bool { |
| return d.fi.IsDir() |
| } |
| |
| func (d *DirEntryFromFileInfo) Type() fs.FileMode { |
| return d.fi.Mode().Type() |
| } |
| |
| func (d *DirEntryFromFileInfo) Info() (fs.FileInfo, error) { |
| return d.fi, nil |
| } |
| |
| func dirEntryFromFileInfo(fi fs.FileInfo) fs.DirEntry { |
| return &DirEntryFromFileInfo{fi} |
| } |
| |
| type DirEntryWithFullPath struct { |
| Entry fs.DirEntry |
| Path string |
| } |
| |
| func hasMetaExceptAlts(s string) bool { |
| var c byte |
| l := len(s) |
| for i := 0; i < l; i++ { |
| c = s[i] |
| if c == '*' || c == '?' || c == '[' { |
| return true |
| } else if c == '\\' { |
| // skip next byte |
| i++ |
| } |
| } |
| return false |
| } |