doublestar-4.6.1/0000755000175000017510000000000014514763347013233 5ustar nileshnileshdoublestar-4.6.1/validate.go0000644000175000017510000000354214514763347015357 0ustar nileshnileshpackage doublestar import "path/filepath" // Validate a pattern. Patterns are validated while they run in Match(), // PathMatch(), and Glob(), so, you normally wouldn't need to call this. // However, there are cases where this might be useful: for example, if your // program allows a user to enter a pattern that you'll run at a later time, // you might want to validate it. // // ValidatePattern assumes your pattern uses '/' as the path separator. // func ValidatePattern(s string) bool { return doValidatePattern(s, '/') } // Like ValidatePattern, only uses your OS path separator. In other words, use // ValidatePattern if you would normally use Match() or Glob(). Use // ValidatePathPattern if you would normally use PathMatch(). Keep in mind, // Glob() requires '/' separators, even if your OS uses something else. // func ValidatePathPattern(s string) bool { return doValidatePattern(s, filepath.Separator) } func doValidatePattern(s string, separator rune) bool { altDepth := 0 l := len(s) VALIDATE: for i := 0; i < l; i++ { switch s[i] { case '\\': if separator != '\\' { // skip the next byte - return false if there is no next byte if i++; i >= l { return false } } continue case '[': if i++; i >= l { // class didn't end return false } if s[i] == '^' || s[i] == '!' { i++ } if i >= l || s[i] == ']' { // class didn't end or empty character class return false } for ; i < l; i++ { if separator != '\\' && s[i] == '\\' { i++ } else if s[i] == ']' { // looks good continue VALIDATE } } // class didn't end return false case '{': altDepth++ continue case '}': if altDepth == 0 { // alt end without a corresponding start return false } altDepth-- continue } } // valid as long as all alts are closed return altDepth == 0 } doublestar-4.6.1/utils_test.go0000644000175000017510000000200714514763347015760 0ustar nileshnileshpackage doublestar import ( "path/filepath" "testing" ) var filepathGlobTests = []string{ ".", "././.", "..", "../.", ".././././", "../..", "/", "./", "/.", "/././././", "nopermission/.", } func TestSpecialFilepathGlobCases(t *testing.T) { for idx, pattern := range filepathGlobTests { testSpecialFilepathGlobCasesWith(t, idx, pattern) } } func testSpecialFilepathGlobCasesWith(t *testing.T, idx int, pattern string) { defer func() { if r := recover(); r != nil { t.Errorf("#%v. FilepathGlob(%#q) panicked with: %#v", idx, pattern, r) } }() pattern = filepath.FromSlash(pattern) matches, err := FilepathGlob(pattern) results, stdErr := filepath.Glob(pattern) // doublestar.FilepathGlob Cleans the path for idx, result := range results { results[idx] = filepath.Clean(result) } if !compareSlices(matches, results) || !compareErrors(err, stdErr) { t.Errorf("#%v. FilepathGlob(%#q) != filepath.Glob(%#q). Got %#v, %v want %#v, %v", idx, pattern, pattern, matches, err, results, stdErr) } } doublestar-4.6.1/utils.go0000644000175000017510000001062714514763347014730 0ustar nileshnileshpackage doublestar import ( "errors" "os" "path" "path/filepath" "strings" ) // SplitPattern is a utility function. Given a pattern, SplitPattern will // return two strings: the first string is everything up to the last slash // (`/`) that appears _before_ any unescaped "meta" characters (ie, `*?[{`). // The second string is everything after that slash. For example, given the // pattern: // // ../../path/to/meta*/** // ^----------- split here // // SplitPattern returns "../../path/to" and "meta*/**". This is useful for // initializing os.DirFS() to call Glob() because Glob() will silently fail if // your pattern includes `/./` or `/../`. For example: // // base, pattern := SplitPattern("../../path/to/meta*/**") // fsys := os.DirFS(base) // matches, err := Glob(fsys, pattern) // // If SplitPattern cannot find somewhere to split the pattern (for example, // `meta*/**`), it will return "." and the unaltered pattern (`meta*/**` in // this example). // // Of course, it is your responsibility to decide if the returned base path is // "safe" in the context of your application. Perhaps you could use Match() to // validate against a list of approved base directories? // func SplitPattern(p string) (base, pattern string) { base = "." pattern = p splitIdx := -1 for i := 0; i < len(p); i++ { c := p[i] if c == '\\' { i++ } else if c == '/' { splitIdx = i } else if c == '*' || c == '?' || c == '[' || c == '{' { break } } if splitIdx == 0 { return "/", p[1:] } else if splitIdx > 0 { return p[:splitIdx], p[splitIdx+1:] } return } // FilepathGlob 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. // // FilepathGlob ignores file system errors such as I/O errors reading // directories by default. The only possible returned error is ErrBadPattern, // reporting that the pattern is malformed. // // To enable aborting on I/O errors, the WithFailOnIOErrors option can be // passed. // // Note: FilepathGlob is a convenience function that is meant as a drop-in // replacement for `path/filepath.Glob()` for users who don't need the // complication of io/fs. Basically, it: // - Runs `filepath.Clean()` and `ToSlash()` on the pattern // - Runs `SplitPattern()` to get a base path and a pattern to Glob // - Creates an FS object from the base path and `Glob()s` on the pattern // - Joins the base path with all of the matches from `Glob()` // // Returned paths will use the system's path separator, just like // `filepath.Glob()`. // // Note: the returned error doublestar.ErrBadPattern is not equal to // filepath.ErrBadPattern. // func FilepathGlob(pattern string, opts ...GlobOption) (matches []string, err error) { pattern = filepath.Clean(pattern) pattern = filepath.ToSlash(pattern) base, f := SplitPattern(pattern) if f == "" || f == "." || f == ".." { // some special cases to match filepath.Glob behavior if !ValidatePathPattern(pattern) { return nil, ErrBadPattern } if filepath.Separator != '\\' { pattern = unescapeMeta(pattern) } if _, err = os.Lstat(pattern); err != nil { g := newGlob(opts...) if errors.Is(err, os.ErrNotExist) { return nil, g.handlePatternNotExist(true) } return nil, g.forwardErrIfFailOnIOErrors(err) } return []string{filepath.FromSlash(pattern)}, nil } fs := os.DirFS(base) if matches, err = Glob(fs, f, opts...); err != nil { return nil, err } for i := range matches { // use path.Join because we used ToSlash above to ensure our paths are made // of forward slashes, no matter what the system uses matches[i] = filepath.FromSlash(path.Join(base, matches[i])) } return } // Finds the next comma, but ignores any commas that appear inside nested `{}`. // Assumes that each opening bracket has a corresponding closing bracket. func indexNextAlt(s string, allowEscaping bool) int { alts := 1 l := len(s) for i := 0; i < l; i++ { if allowEscaping && s[i] == '\\' { // skip next byte i++ } else if s[i] == '{' { alts++ } else if s[i] == '}' { alts-- } else if s[i] == ',' && alts == 1 { return i } } return -1 } var metaReplacer = strings.NewReplacer("\\*", "*", "\\?", "?", "\\[", "[", "\\]", "]", "\\{", "{", "\\}", "}") // Unescapes meta characters (*?[]{}) func unescapeMeta(pattern string) string { return metaReplacer.Replace(pattern) } doublestar-4.6.1/match.go0000644000175000017510000002644614514763347014672 0ustar nileshnileshpackage doublestar import ( "path/filepath" "unicode/utf8" ) // Match reports whether name matches the shell pattern. // The pattern syntax is: // // pattern: // { term } // term: // '*' matches any sequence of non-path-separators // '/**/' matches zero or more directories // '?' matches any single non-path-separator character // '[' [ '^' '!' ] { character-range } ']' // character class (must be non-empty) // starting with `^` or `!` negates the class // '{' { term } [ ',' { term } ... ] '}' // alternatives // 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 returns true if `name` matches the file name `pattern`. `name` and // `pattern` are split on forward slash (`/`) characters and may be relative or // absolute. // // Match requires pattern to match all of name, not just a substring. // The only possible returned error is ErrBadPattern, when pattern // is malformed. // // A doublestar (`**`) should appear surrounded by path separators such as // `/**/`. 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`. // // 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 PathMatch(). Alternatively, you can run filepath.ToSlash() on both // pattern and name and then use this function. // // Note: users should _not_ count on the returned error, // doublestar.ErrBadPattern, being equal to path.ErrBadPattern. // func Match(pattern, name string) (bool, error) { return matchWithSeparator(pattern, name, '/', true) } // PathMatch returns true if `name` matches the file name `pattern`. The // difference between Match and PathMatch is that PathMatch will automatically // use your system's path separator to split `name` and `pattern`. On systems // where the path separator is `'\'`, escaping will be disabled. // // Note: this is meant as a drop-in replacement for filepath.Match(). It // assumes that both `pattern` and `name` are using the system's path // separator. If you can't be sure of that, use filepath.ToSlash() on both // `pattern` and `name`, and then use the Match() function instead. // func PathMatch(pattern, name string) (bool, error) { return matchWithSeparator(pattern, name, filepath.Separator, true) } func matchWithSeparator(pattern, name string, separator rune, validate bool) (matched bool, err error) { return doMatchWithSeparator(pattern, name, separator, validate, -1, -1, -1, -1, 0, 0) } func doMatchWithSeparator(pattern, name string, separator rune, validate bool, doublestarPatternBacktrack, doublestarNameBacktrack, starPatternBacktrack, starNameBacktrack, patIdx, nameIdx int) (matched bool, err error) { patLen := len(pattern) nameLen := len(name) startOfSegment := true MATCH: for nameIdx < nameLen { if patIdx < patLen { switch pattern[patIdx] { case '*': if patIdx++; patIdx < patLen && pattern[patIdx] == '*' { // doublestar - must begin with a path separator, otherwise we'll // treat it like a single star like bash patIdx++ if startOfSegment { if patIdx >= patLen { // pattern ends in `/**`: return true return true, nil } // doublestar must also end with a path separator, otherwise we're // just going to treat the doublestar as a single star like bash patRune, patRuneLen := utf8.DecodeRuneInString(pattern[patIdx:]) if patRune == separator { patIdx += patRuneLen doublestarPatternBacktrack = patIdx doublestarNameBacktrack = nameIdx starPatternBacktrack = -1 starNameBacktrack = -1 continue } } } startOfSegment = false starPatternBacktrack = patIdx starNameBacktrack = nameIdx continue case '?': startOfSegment = false nameRune, nameRuneLen := utf8.DecodeRuneInString(name[nameIdx:]) if nameRune == separator { // `?` cannot match the separator break } patIdx++ nameIdx += nameRuneLen continue case '[': startOfSegment = false if patIdx++; patIdx >= patLen { // class didn't end return false, ErrBadPattern } nameRune, nameRuneLen := utf8.DecodeRuneInString(name[nameIdx:]) matched := false negate := pattern[patIdx] == '!' || pattern[patIdx] == '^' if negate { patIdx++ } if patIdx >= patLen || pattern[patIdx] == ']' { // class didn't end or empty character class return false, ErrBadPattern } last := utf8.MaxRune for patIdx < patLen && pattern[patIdx] != ']' { patRune, patRuneLen := utf8.DecodeRuneInString(pattern[patIdx:]) patIdx += patRuneLen // match a range if last < utf8.MaxRune && patRune == '-' && patIdx < patLen && pattern[patIdx] != ']' { if pattern[patIdx] == '\\' { // next character is escaped patIdx++ } patRune, patRuneLen = utf8.DecodeRuneInString(pattern[patIdx:]) patIdx += patRuneLen if last <= nameRune && nameRune <= patRune { matched = true break } // didn't match range - reset `last` last = utf8.MaxRune continue } // not a range - check if the next rune is escaped if patRune == '\\' { patRune, patRuneLen = utf8.DecodeRuneInString(pattern[patIdx:]) patIdx += patRuneLen } // check if the rune matches if patRune == nameRune { matched = true break } // no matches yet last = patRune } if matched == negate { // failed to match - if we reached the end of the pattern, that means // we never found a closing `]` if patIdx >= patLen { return false, ErrBadPattern } break } closingIdx := indexUnescapedByte(pattern[patIdx:], ']', true) if closingIdx == -1 { // no closing `]` return false, ErrBadPattern } patIdx += closingIdx + 1 nameIdx += nameRuneLen continue case '{': startOfSegment = false beforeIdx := patIdx patIdx++ closingIdx := indexMatchedClosingAlt(pattern[patIdx:], separator != '\\') if closingIdx == -1 { // no closing `}` return false, ErrBadPattern } closingIdx += patIdx for { commaIdx := indexNextAlt(pattern[patIdx:closingIdx], separator != '\\') if commaIdx == -1 { break } commaIdx += patIdx result, err := doMatchWithSeparator(pattern[:beforeIdx]+pattern[patIdx:commaIdx]+pattern[closingIdx+1:], name, separator, validate, doublestarPatternBacktrack, doublestarNameBacktrack, starPatternBacktrack, starNameBacktrack, beforeIdx, nameIdx) if result || err != nil { return result, err } patIdx = commaIdx + 1 } return doMatchWithSeparator(pattern[:beforeIdx]+pattern[patIdx:closingIdx]+pattern[closingIdx+1:], name, separator, validate, doublestarPatternBacktrack, doublestarNameBacktrack, starPatternBacktrack, starNameBacktrack, beforeIdx, nameIdx) case '\\': if separator != '\\' { // next rune is "escaped" in the pattern - literal match if patIdx++; patIdx >= patLen { // pattern ended return false, ErrBadPattern } } fallthrough default: patRune, patRuneLen := utf8.DecodeRuneInString(pattern[patIdx:]) nameRune, nameRuneLen := utf8.DecodeRuneInString(name[nameIdx:]) if patRune != nameRune { if separator != '\\' && patIdx > 0 && pattern[patIdx-1] == '\\' { // if this rune was meant to be escaped, we need to move patIdx // back to the backslash before backtracking or validating below patIdx-- } break } patIdx += patRuneLen nameIdx += nameRuneLen startOfSegment = patRune == separator continue } } if starPatternBacktrack >= 0 { // `*` backtrack, but only if the `name` rune isn't the separator nameRune, nameRuneLen := utf8.DecodeRuneInString(name[starNameBacktrack:]) if nameRune != separator { starNameBacktrack += nameRuneLen patIdx = starPatternBacktrack nameIdx = starNameBacktrack startOfSegment = false continue } } if doublestarPatternBacktrack >= 0 { // `**` backtrack, advance `name` past next separator nameIdx = doublestarNameBacktrack for nameIdx < nameLen { nameRune, nameRuneLen := utf8.DecodeRuneInString(name[nameIdx:]) nameIdx += nameRuneLen if nameRune == separator { doublestarNameBacktrack = nameIdx patIdx = doublestarPatternBacktrack startOfSegment = true continue MATCH } } } if validate && patIdx < patLen && !doValidatePattern(pattern[patIdx:], separator) { return false, ErrBadPattern } return false, nil } if nameIdx < nameLen { // we reached the end of `pattern` before the end of `name` return false, nil } // we've reached the end of `name`; we've successfully matched if we've also // reached the end of `pattern`, or if the rest of `pattern` can match a // zero-length string return isZeroLengthPattern(pattern[patIdx:], separator) } func isZeroLengthPattern(pattern string, separator rune) (ret bool, err error) { // `/**`, `**/`, and `/**/` are special cases - a pattern such as `path/to/a/**` or `path/to/a/**/` // *should* match `path/to/a` because `a` might be a directory if pattern == "" || pattern == "*" || pattern == "**" || pattern == string(separator)+"**" || pattern == "**"+string(separator) || pattern == string(separator)+"**"+string(separator) { return true, nil } if pattern[0] == '{' { closingIdx := indexMatchedClosingAlt(pattern[1:], separator != '\\') if closingIdx == -1 { // no closing '}' return false, ErrBadPattern } closingIdx += 1 patIdx := 1 for { commaIdx := indexNextAlt(pattern[patIdx:closingIdx], separator != '\\') if commaIdx == -1 { break } commaIdx += patIdx ret, err = isZeroLengthPattern(pattern[patIdx:commaIdx]+pattern[closingIdx+1:], separator) if ret || err != nil { return } patIdx = commaIdx + 1 } return isZeroLengthPattern(pattern[patIdx:closingIdx]+pattern[closingIdx+1:], separator) } // no luck - validate the rest of the pattern if !doValidatePattern(pattern, separator) { return false, ErrBadPattern } return false, nil } // Finds the index of the first unescaped byte `c`, or negative 1. func indexUnescapedByte(s string, c byte, allowEscaping bool) int { l := len(s) for i := 0; i < l; i++ { if allowEscaping && s[i] == '\\' { // skip next byte i++ } else if s[i] == c { return i } } return -1 } // Assuming the byte before the beginning of `s` is an opening `{`, this // function will find the index of the matching `}`. That is, it'll skip over // any nested `{}` and account for escaping func indexMatchedClosingAlt(s string, allowEscaping bool) int { alts := 1 l := len(s) for i := 0; i < l; i++ { if allowEscaping && s[i] == '\\' { // skip next byte i++ } else if s[i] == '{' { alts++ } else if s[i] == '}' { if alts--; alts == 0 { return i } } } return -1 } doublestar-4.6.1/go.mod0000644000175000017510000000006114514763347014336 0ustar nileshnileshmodule github.com/bmatcuk/doublestar/v4 go 1.16 doublestar-4.6.1/globwalk_test.go0000644000175000017510000000312514514763347016424 0ustar nileshnileshpackage doublestar import ( "io/fs" "os" "testing" ) type SkipTest struct { pattern string // pattern to test skipOn string // a path to skip shouldNotContain string // a path that should not match numResults int // number of expected matches winNumResults int // number of expected matches on windows } var skipTests = []SkipTest{ {"a", "a", "a", 0, 0}, {"a/", "a", "a", 1, 1}, {"*", "b", "c", 11, 9}, {"a/**", "a", "a", 0, 0}, {"a/**", "a/abc", "a/b", 1, 1}, {"a/**", "a/b/c", "a/b/c/d", 5, 5}, {"a/{**,c/*}", "a/b/c", "a/b/c/d", 5, 5}, {"a/{**,c/*}", "a/abc", "a/b", 1, 1}, } func TestSkipDirInGlobWalk(t *testing.T) { fsys := os.DirFS("test") for idx, tt := range skipTests { testSkipDirInGlobWalkWith(t, idx, tt, fsys) } } func testSkipDirInGlobWalkWith(t *testing.T, idx int, tt SkipTest, fsys fs.FS) { defer func() { if r := recover(); r != nil { t.Errorf("#%v. GlobWalk(%#q) panicked: %#v", idx, tt.pattern, r) } }() var matches []string hadBadMatch := false GlobWalk(fsys, tt.pattern, func(p string, d fs.DirEntry) error { if p == tt.skipOn { return SkipDir } if p == tt.shouldNotContain { hadBadMatch = true } matches = append(matches, p) return nil }) expected := tt.numResults if onWindows { expected = tt.winNumResults } if len(matches) != expected { t.Errorf("#%v. GlobWalk(%#q) = %#v - should have %#v results, got %#v", idx, tt.pattern, matches, expected, len(matches)) } if hadBadMatch { t.Errorf("#%v. GlobWalk(%#q) should not have matched %#q, but did", idx, tt.pattern, tt.shouldNotContain) } } doublestar-4.6.1/globwalk.go0000644000175000017510000003010014514763347015356 0ustar nileshnileshpackage 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 } doublestar-4.6.1/globoptions.go0000644000175000017510000001015414514763347016122 0ustar nileshnileshpackage doublestar import "strings" // glob is an internal type to store options during globbing. type glob struct { failOnIOErrors bool failOnPatternNotExist bool filesOnly bool noFollow bool } // GlobOption represents a setting that can be passed to Glob, GlobWalk, and // FilepathGlob. type GlobOption func(*glob) // Construct a new glob object with the given options func newGlob(opts ...GlobOption) *glob { g := &glob{} for _, opt := range opts { opt(g) } return g } // WithFailOnIOErrors is an option that can be passed to Glob, GlobWalk, or // FilepathGlob. If passed, doublestar will abort and return IO errors when // encountered. Note that if the glob pattern references a path that does not // exist (such as `nonexistent/path/*`), this is _not_ considered an IO error: // it is considered a pattern with no matches. // func WithFailOnIOErrors() GlobOption { return func(g *glob) { g.failOnIOErrors = true } } // WithFailOnPatternNotExist is an option that can be passed to Glob, GlobWalk, // or FilepathGlob. If passed, doublestar will abort and return // ErrPatternNotExist if the pattern references a path that does not exist // before any meta charcters such as `nonexistent/path/*`. Note that alts (ie, // `{...}`) are expanded before this check. In other words, a pattern such as // `{a,b}/*` may fail if either `a` or `b` do not exist but `*/{a,b}` will // never fail because the star may match nothing. // func WithFailOnPatternNotExist() GlobOption { return func(g *glob) { g.failOnPatternNotExist = true } } // WithFilesOnly is an option that can be passed to Glob, GlobWalk, or // FilepathGlob. If passed, doublestar will only return files that match the // pattern, not directories. // // Note: if combined with the WithNoFollow option, symlinks to directories // _will_ be included in the result since no attempt is made to follow the // symlink. // func WithFilesOnly() GlobOption { return func(g *glob) { g.filesOnly = true } } // WithNoFollow is an option that can be passed to Glob, GlobWalk, or // FilepathGlob. If passed, doublestar will not follow symlinks while // traversing the filesystem. However, due to io/fs's _very_ poor support for // querying the filesystem about symlinks, there's a caveat here: if part of // the pattern before any meta characters contains a reference to a symlink, it // will be followed. For example, a pattern such as `path/to/symlink/*` will be // followed assuming it is a valid symlink to a directory. However, from this // same example, a pattern such as `path/to/**` will not traverse the // `symlink`, nor would `path/*/symlink/*` // // Note: if combined with the WithFilesOnly option, symlinks to directories // _will_ be included in the result since no attempt is made to follow the // symlink. // func WithNoFollow() GlobOption { return func(g *glob) { g.noFollow = true } } // forwardErrIfFailOnIOErrors is used to wrap the return values of I/O // functions. When failOnIOErrors is enabled, it will return err; otherwise, it // always returns nil. // func (g *glob) forwardErrIfFailOnIOErrors(err error) error { if g.failOnIOErrors { return err } return nil } // handleErrNotExist handles fs.ErrNotExist errors. If // WithFailOnPatternNotExist has been enabled and canFail is true, this will // return ErrPatternNotExist. Otherwise, it will return nil. // func (g *glob) handlePatternNotExist(canFail bool) error { if canFail && g.failOnPatternNotExist { return ErrPatternNotExist } return nil } // Format options for debugging/testing purposes func (g *glob) GoString() string { var b strings.Builder b.WriteString("opts: ") hasOpts := false if g.failOnIOErrors { b.WriteString("WithFailOnIOErrors") hasOpts = true } if g.failOnPatternNotExist { if hasOpts { b.WriteString(", ") } b.WriteString("WithFailOnPatternNotExist") hasOpts = true } if g.filesOnly { if hasOpts { b.WriteString(", ") } b.WriteString("WithFilesOnly") hasOpts = true } if g.noFollow { if hasOpts { b.WriteString(", ") } b.WriteString("WithNoFollow") hasOpts = true } if !hasOpts { b.WriteString("nil") } return b.String() } doublestar-4.6.1/glob.go0000644000175000017510000003416414514763347014515 0ustar nileshnileshpackage doublestar import ( "errors" "io/fs" "path" ) // 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. // // Glob ignores file system errors such as I/O errors reading directories by // default. The only possible returned error is ErrBadPattern, reporting that // the pattern is malformed. // // To enable aborting on I/O errors, the WithFailOnIOErrors option can be // passed. // // Note: this is meant as a drop-in replacement for io/fs.Glob(). Like // io/fs.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 Glob(). // // Like `io/fs.Glob()`, patterns containing `/./`, `/../`, or starting with `/` // will return no results and no errors. You can use SplitPattern to divide a // pattern into a base path (to initialize an `FS` object) and pattern. // // Note: users should _not_ count on the returned error, // doublestar.ErrBadPattern, being equal to path.ErrBadPattern. // func Glob(fsys fs.FS, pattern string, opts ...GlobOption) ([]string, error) { if !ValidatePattern(pattern) { return nil, ErrBadPattern } g := newGlob(opts...) if hasMidDoubleStar(pattern) { // If the pattern has a `**` anywhere but the very end, GlobWalk is more // performant because it can get away with less allocations. If the pattern // ends in a `**`, both methods are pretty much the same, but Glob has a // _very_ slight advantage because of lower function call overhead. var matches []string err := g.doGlobWalk(fsys, pattern, true, true, func(p string, d fs.DirEntry) error { matches = append(matches, p) return nil }) return matches, err } return g.doGlob(fsys, pattern, nil, true, true) } // Does the actual globbin' // - 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) doGlob(fsys fs.FS, pattern string, m []string, firstSegment, beforeMeta bool) (matches []string, err error) { matches = m 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) pathInfo, pathExists, pathErr := g.exists(fsys, path, beforeMeta) if pathErr != nil { return nil, pathErr } if pathExists && (!firstSegment || !g.filesOnly || !pathInfo.IsDir()) { matches = append(matches, path) } return } 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.globAlts(fsys, pattern, openingIdx, splitIdx, matches, firstSegment, beforeMeta) } } 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.globDir(fsys, dir, pattern, matches, firstSegment, beforeMeta) } var dirs []string dirs, err = g.doGlob(fsys, dir, matches, false, beforeMeta) if err != nil { return } for _, d := range dirs { matches, err = g.globDir(fsys, d, pattern, matches, firstSegment, false) if err != nil { return } } return } // handle alts in the glob pattern - `openingIdx` and `closingIdx` are the // indexes of `{` and `}`, respectively func (g *glob) globAlts(fsys fs.FS, pattern string, openingIdx, closingIdx int, m []string, firstSegment, beforeMeta bool) (matches []string, err error) { matches = m var dirs []string startIdx := 0 afterIdx := closingIdx + 1 splitIdx := lastIndexSlashOrAlt(pattern[:openingIdx]) if splitIdx == -1 || pattern[splitIdx] == '}' { // no common prefix dirs = []string{""} } else { // our alts have a common prefix that we can process first dirs, err = g.doGlob(fsys, pattern[:splitIdx], matches, false, beforeMeta) if err != nil { return } startIdx = splitIdx + 1 } for _, d := range dirs { patIdx := openingIdx + 1 altResultsStartIdx := len(matches) thisResultStartIdx := altResultsStartIdx 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) matches, err = g.doGlob(fsys, alt, matches, firstSegment, beforeMeta) if err != nil { return } matchesLen := len(matches) if altResultsStartIdx != thisResultStartIdx && thisResultStartIdx != matchesLen { // Alts can result in matches that aren't sorted, or, worse, duplicates // (consider the trivial pattern `path/to/{a,*}`). Since doGlob returns // sorted results, we can do a sort of in-place merge and remove // duplicates. But, we only need to do this if this isn't the first alt // (ie, `altResultsStartIdx != thisResultsStartIdx`) and if the latest // alt actually added some matches (`thisResultStartIdx != // len(matches)`) matches = sortAndRemoveDups(matches, altResultsStartIdx, thisResultStartIdx, matchesLen) // length of matches may have changed thisResultStartIdx = len(matches) } else { thisResultStartIdx = matchesLen } patIdx = nextIdx + 1 } } return } // find files/subdirectories in the given `dir` that match `pattern` func (g *glob) globDir(fsys fs.FS, dir, pattern string, matches []string, canMatchFiles, beforeMeta bool) (m []string, e error) { m = matches 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) _, isDir, err := g.isPathDir(fsys, dir, beforeMeta) if err != nil { return nil, err } if isDir { m = append(m, dir) } } return } if pattern == "**" { return g.globDoubleStar(fsys, dir, m, canMatchFiles, beforeMeta) } dirs, err := fs.ReadDir(fsys, dir) if err != nil { if errors.Is(err, fs.ErrNotExist) { e = g.handlePatternNotExist(beforeMeta) } else { e = g.forwardErrIfFailOnIOErrors(err) } return } 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 } if canMatchFiles { // if we're here, it's because g.filesOnly // is set and we don't want directories matched = !matched } } if matched { m = append(m, path.Join(dir, name)) } } } return } func (g *glob) globDoubleStar(fsys fs.FS, dir string, matches []string, canMatchFiles, beforeMeta bool) ([]string, error) { dirs, err := fs.ReadDir(fsys, dir) if err != nil { if errors.Is(err, fs.ErrNotExist) { return matches, g.handlePatternNotExist(beforeMeta) } else { return matches, g.forwardErrIfFailOnIOErrors(err) } } if !g.filesOnly { // `**` can match *this* dir, so add it matches = append(matches, dir) } for _, info := range dirs { name := info.Name() isDir, err := g.isDir(fsys, dir, name, info) if err != nil { return nil, err } if isDir { matches, err = g.globDoubleStar(fsys, path.Join(dir, name), matches, canMatchFiles, false) if err != nil { return nil, err } } else if canMatchFiles { matches = append(matches, path.Join(dir, name)) } } return matches, nil } // Returns true if the pattern has a doublestar in the middle of the pattern. // In this case, GlobWalk is faster because it can get away with less // allocations. However, Glob has a _very_ slight edge if the pattern ends in // `**`. func hasMidDoubleStar(p string) bool { // subtract 3: 2 because we want to return false if the pattern ends in `**` // (Glob is _very_ slightly faster in that case), and the extra 1 because our // loop checks p[i] and p[i+1]. l := len(p) - 3 for i := 0; i < l; i++ { if p[i] == '\\' { // escape next byte i++ } else if p[i] == '*' && p[i+1] == '*' { return true } } return false } // Returns the index of the first unescaped meta character, or negative 1. func indexMeta(s string) int { var c byte l := len(s) for i := 0; i < l; i++ { c = s[i] if c == '*' || c == '?' || c == '[' || c == '{' { return i } else if c == '\\' { // skip next byte i++ } } return -1 } // Returns the index of the last unescaped slash or closing alt (`}`) in the // string, or negative 1. func lastIndexSlashOrAlt(s string) int { for i := len(s) - 1; i >= 0; i-- { if (s[i] == '/' || s[i] == '}') && (i == 0 || s[i-1] != '\\') { return i } } return -1 } // Returns the index of the last unescaped slash in the string, or negative 1. func lastIndexSlash(s string) int { for i := len(s) - 1; i >= 0; i-- { if s[i] == '/' && (i == 0 || s[i-1] != '\\') { return i } } return -1 } // Assuming the byte after the end of `s` is a closing `}`, this function will // find the index of the matching `{`. That is, it'll skip over any nested `{}` // and account for escaping. func indexMatchedOpeningAlt(s string) int { alts := 1 for i := len(s) - 1; i >= 0; i-- { if s[i] == '}' && (i == 0 || s[i-1] != '\\') { alts++ } else if s[i] == '{' && (i == 0 || s[i-1] != '\\') { if alts--; alts == 0 { return i } } } return -1 } // Returns true if the path exists func (g *glob) exists(fsys fs.FS, name string, beforeMeta bool) (fs.FileInfo, bool, error) { // name might end in a slash, but Stat doesn't like that namelen := len(name) if namelen > 1 && name[namelen-1] == '/' { name = name[:namelen-1] } info, err := fs.Stat(fsys, name) if errors.Is(err, fs.ErrNotExist) { return nil, false, g.handlePatternNotExist(beforeMeta) } return info, err == nil, g.forwardErrIfFailOnIOErrors(err) } // Returns true if the path exists and is a directory or a symlink to a // directory func (g *glob) isPathDir(fsys fs.FS, name string, beforeMeta bool) (fs.FileInfo, bool, error) { info, err := fs.Stat(fsys, name) if errors.Is(err, fs.ErrNotExist) { return nil, false, g.handlePatternNotExist(beforeMeta) } return info, err == nil && info.IsDir(), g.forwardErrIfFailOnIOErrors(err) } // Returns whether or not the given DirEntry is a directory. If the DirEntry // represents a symbolic link, the link is followed by running fs.Stat() on // `path.Join(dir, name)` (if dir is "", name will be used without joining) func (g *glob) isDir(fsys fs.FS, dir, name string, info fs.DirEntry) (bool, error) { if !g.noFollow && (info.Type()&fs.ModeSymlink) > 0 { p := name if dir != "" { p = path.Join(dir, name) } finfo, err := fs.Stat(fsys, p) if err != nil { if errors.Is(err, fs.ErrNotExist) { // this function is only ever called while expanding a glob, so it can // never return ErrPatternNotExist return false, nil } return false, g.forwardErrIfFailOnIOErrors(err) } return finfo.IsDir(), nil } return info.IsDir(), nil } // Builds a string from an alt func buildAlt(prefix, pattern string, startIdx, openingIdx, currentIdx, nextIdx, afterIdx int) string { // pattern: // ignored/start{alts,go,here}remaining - len = 36 // | | | | ^--- afterIdx = 27 // | | | \--------- nextIdx = 21 // | | \----------- currentIdx = 19 // | \----------------- openingIdx = 13 // \---------------------- startIdx = 8 // // result: // prefix/startgoremaining - len = 7 + 5 + 2 + 9 = 23 var buf []byte patLen := len(pattern) size := (openingIdx - startIdx) + (nextIdx - currentIdx) + (patLen - afterIdx) if prefix != "" && prefix != "." { buf = make([]byte, 0, size+len(prefix)+1) buf = append(buf, prefix...) buf = append(buf, '/') } else { buf = make([]byte, 0, size) } buf = append(buf, pattern[startIdx:openingIdx]...) buf = append(buf, pattern[currentIdx:nextIdx]...) if afterIdx < patLen { buf = append(buf, pattern[afterIdx:]...) } return string(buf) } // Running alts can produce results that are not sorted, and, worse, can cause // duplicates (consider the trivial pattern `path/to/{a,*}`). Since we know // each run of doGlob is sorted, we can basically do the "merge" step of a // merge sort in-place. func sortAndRemoveDups(matches []string, idx1, idx2, l int) []string { var tmp string for ; idx1 < idx2; idx1++ { if matches[idx1] < matches[idx2] { // order is correct continue } else if matches[idx1] > matches[idx2] { // need to swap and then re-sort matches above idx2 tmp = matches[idx1] matches[idx1] = matches[idx2] shft := idx2 + 1 for ; shft < l && matches[shft] < tmp; shft++ { matches[shft-1] = matches[shft] } matches[shft-1] = tmp } else { // duplicate - shift matches above idx2 down one and decrement l for shft := idx2 + 1; shft < l; shft++ { matches[shft-1] = matches[shft] } if l--; idx2 == l { // nothing left to do... matches[idx2:] must have been full of dups break } } } return matches[:l] } doublestar-4.6.1/examples/0000755000175000017510000000000014514763347015051 5ustar nileshnileshdoublestar-4.6.1/examples/go.mod0000644000175000017510000000024414514763347016157 0ustar nileshnileshmodule github.com/bmatcuk/doublestar/examples/find replace github.com/bmatcuk/doublestar/v4 => ../ require ( github.com/bmatcuk/doublestar/v4 v4.0.0 ) go 1.16 doublestar-4.6.1/examples/find.go0000644000175000017510000000210514514763347016316 0ustar nileshnileshpackage main import ( "fmt" "os" "strings" "github.com/bmatcuk/doublestar/v4" ) // To run: // $ go run find.go // // For example: // $ go run find.go '/usr/bin/* // // Make sure to escape the pattern as necessary for your shell, otherwise the // shell will expand the pattern! Additionally, you should use `/` as the path // separator even if your OS (like Windows) does not! // // Patterns that include `.` or `..` after any meta characters (*, ?, [, or {) // will not work because io/fs will reject them. If they appear _before_ any // meta characters _and_ before a `/`, the `splitPattern` function below will // take care of them correctly. func main() { pattern := os.Args[1] fmt.Printf("Searching on disk for pattern: %s\n\n", pattern) var basepath string basepath, pattern = doublestar.SplitPattern(pattern) fsys := os.DirFS(basepath) matches, err := doublestar.Glob(fsys, 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-4.6.1/doublestar_test.go0000644000175000017510000007226414514763347017000 0ustar nileshnileshpackage doublestar import ( "io/fs" "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 shouldMatchGlob bool // true if glob should match the path expectedErr error // an expected error expectIOErr bool // whether or not to expect an io error expectPatternNotExist bool // whether or not to expect ErrPatternNotExist isStandard bool // pattern doesn't use any doublestar features (e.g. '**', '{a,b}') testOnDisk bool // true: test pattern against files in "test" directory numResults int // number of glob results if testing on disk winNumResults int // number of glob results on Windows } // Tests which contain escapes and symlinks will not work on Windows var onWindows = runtime.GOOS == "windows" var matchTests = []MatchTest{ {"*", "", true, true, nil, false, false, true, false, 0, 0}, {"*", "/", false, false, nil, false, false, true, false, 0, 0}, {"/*", "/", true, true, nil, false, false, true, false, 0, 0}, {"/*", "/debug/", false, false, nil, false, false, true, false, 0, 0}, {"/*", "//", false, false, nil, false, false, true, false, 0, 0}, {"abc", "abc", true, true, nil, false, false, true, true, 1, 1}, {"*", "abc", true, true, nil, false, false, true, true, 22, 17}, {"*c", "abc", true, true, nil, false, false, true, true, 2, 2}, {"*/", "a/", true, true, nil, false, false, true, false, 0, 0}, {"a*", "a", true, true, nil, false, false, true, true, 9, 9}, {"a*", "abc", true, true, nil, false, false, true, true, 9, 9}, {"a*", "ab/c", false, false, nil, false, false, true, true, 9, 9}, {"a*/b", "abc/b", true, true, nil, false, false, true, true, 2, 2}, {"a*/b", "a/c/b", false, false, nil, false, false, true, true, 2, 2}, {"a*/c/", "a/b", false, false, nil, false, false, false, true, 1, 1}, {"a*b*c*d*e*", "axbxcxdxe", true, true, nil, false, false, true, true, 3, 3}, {"a*b*c*d*e*/f", "axbxcxdxe/f", true, true, nil, false, false, true, true, 2, 2}, {"a*b*c*d*e*/f", "axbxcxdxexxx/f", true, true, nil, false, false, true, true, 2, 2}, {"a*b*c*d*e*/f", "axbxcxdxe/xxx/f", false, false, nil, false, false, true, true, 2, 2}, {"a*b*c*d*e*/f", "axbxcxdxexxx/fff", false, false, nil, false, false, true, true, 2, 2}, {"a*b?c*x", "abxbbxdbxebxczzx", true, true, nil, false, false, true, true, 2, 2}, {"a*b?c*x", "abxbbxdbxebxczzy", false, false, nil, false, false, true, true, 2, 2}, {"ab[c]", "abc", true, true, nil, false, false, true, true, 1, 1}, {"ab[b-d]", "abc", true, true, nil, false, false, true, true, 1, 1}, {"ab[e-g]", "abc", false, false, nil, false, false, true, true, 0, 0}, {"ab[^c]", "abc", false, false, nil, false, false, true, true, 0, 0}, {"ab[^b-d]", "abc", false, false, nil, false, false, true, true, 0, 0}, {"ab[^e-g]", "abc", true, true, nil, false, false, true, true, 1, 1}, {"a\\*b", "ab", false, false, nil, false, true, true, !onWindows, 0, 0}, {"a?b", "a☺b", true, true, nil, false, false, true, true, 1, 1}, {"a[^a]b", "a☺b", true, true, nil, false, false, true, true, 1, 1}, {"a[!a]b", "a☺b", true, true, nil, false, false, false, true, 1, 1}, {"a???b", "a☺b", false, false, nil, false, false, true, true, 0, 0}, {"a[^a][^a][^a]b", "a☺b", false, false, nil, false, false, true, true, 0, 0}, {"[a-ζ]*", "α", true, true, nil, false, false, true, true, 20, 17}, {"*[a-ζ]", "A", false, false, nil, false, false, true, true, 20, 17}, {"a?b", "a/b", false, false, nil, false, false, true, true, 1, 1}, {"a*b", "a/b", false, false, nil, false, false, true, true, 1, 1}, {"[\\]a]", "]", true, true, nil, false, false, true, !onWindows, 2, 2}, {"[\\-]", "-", true, true, nil, false, false, true, !onWindows, 1, 1}, {"[x\\-]", "x", true, true, nil, false, false, true, !onWindows, 2, 2}, {"[x\\-]", "-", true, true, nil, false, false, true, !onWindows, 2, 2}, {"[x\\-]", "z", false, false, nil, false, false, true, !onWindows, 2, 2}, {"[\\-x]", "x", true, true, nil, false, false, true, !onWindows, 2, 2}, {"[\\-x]", "-", true, true, nil, false, false, true, !onWindows, 2, 2}, {"[\\-x]", "a", false, false, nil, false, false, true, !onWindows, 2, 2}, {"[]a]", "]", false, false, ErrBadPattern, false, false, true, true, 0, 0}, // doublestar, like bash, allows these when path.Match() does not {"[-]", "-", true, true, nil, false, false, false, !onWindows, 1, 0}, {"[x-]", "x", true, true, nil, false, false, false, true, 2, 1}, {"[x-]", "-", true, true, nil, false, false, false, !onWindows, 2, 1}, {"[x-]", "z", false, false, nil, false, false, false, true, 2, 1}, {"[-x]", "x", true, true, nil, false, false, false, true, 2, 1}, {"[-x]", "-", true, true, nil, false, false, false, !onWindows, 2, 1}, {"[-x]", "a", false, false, nil, false, false, false, true, 2, 1}, {"[a-b-d]", "a", true, true, nil, false, false, false, true, 3, 2}, {"[a-b-d]", "b", true, true, nil, false, false, false, true, 3, 2}, {"[a-b-d]", "-", true, true, nil, false, false, false, !onWindows, 3, 2}, {"[a-b-d]", "c", false, false, nil, false, false, false, true, 3, 2}, {"[a-b-x]", "x", true, true, nil, false, false, false, true, 4, 3}, {"\\", "a", false, false, ErrBadPattern, false, false, true, !onWindows, 0, 0}, {"[", "a", false, false, ErrBadPattern, false, false, true, true, 0, 0}, {"[^", "a", false, false, ErrBadPattern, false, false, true, true, 0, 0}, {"[^bc", "a", false, false, ErrBadPattern, false, false, true, true, 0, 0}, {"a[", "a", false, false, ErrBadPattern, false, false, true, true, 0, 0}, {"a[", "ab", false, false, ErrBadPattern, false, false, true, true, 0, 0}, {"ad[", "ab", false, false, ErrBadPattern, false, false, true, true, 0, 0}, {"*x", "xxx", true, true, nil, false, false, true, true, 4, 4}, {"[abc]", "b", true, true, nil, false, false, true, true, 3, 3}, {"**", "", true, true, nil, false, false, false, false, 38, 38}, {"a/**", "a", true, true, nil, false, false, false, true, 7, 7}, {"a/**/", "a", true, true, nil, false, false, false, true, 4, 4}, {"a/**", "a/", true, true, nil, false, false, false, false, 7, 7}, {"a/**/", "a/", true, true, nil, false, false, false, false, 4, 4}, {"a/**", "a/b", true, true, nil, false, false, false, true, 7, 7}, {"a/**", "a/b/c", true, true, nil, false, false, false, true, 7, 7}, {"**/c", "c", true, true, nil, !onWindows, false, false, true, 5, 4}, {"**/c", "b/c", true, true, nil, !onWindows, false, false, true, 5, 4}, {"**/c", "a/b/c", true, true, nil, !onWindows, false, false, true, 5, 4}, {"**/c", "a/b", false, false, nil, !onWindows, false, false, true, 5, 4}, {"**/c", "abcd", false, false, nil, !onWindows, false, false, true, 5, 4}, {"**/c", "a/abc", false, false, nil, !onWindows, false, false, true, 5, 4}, {"a/**/b", "a/b", true, true, nil, false, false, false, true, 2, 2}, {"a/**/c", "a/b/c", true, true, nil, false, false, false, true, 2, 2}, {"a/**/d", "a/b/c/d", true, true, nil, false, false, false, true, 1, 1}, {"a/\\**", "a/b/c", false, false, nil, false, false, false, !onWindows, 0, 0}, {"a/\\[*\\]", "a/bc", false, false, nil, false, false, true, !onWindows, 0, 0}, // this fails the FilepathGlob test on Windows {"a/b/c", "a/b//c", false, false, nil, false, false, true, !onWindows, 1, 1}, // odd: Glob + filepath.Glob return results {"a/", "a", false, false, nil, false, false, true, false, 0, 0}, {"ab{c,d}", "abc", true, true, nil, false, true, false, true, 1, 1}, {"ab{c,d,*}", "abcde", true, true, nil, false, true, false, true, 5, 5}, {"ab{c,d}[", "abcd", false, false, ErrBadPattern, false, false, false, true, 0, 0}, {"a{,bc}", "a", true, true, nil, false, false, false, true, 2, 2}, {"a{,bc}", "abc", true, true, nil, false, false, false, true, 2, 2}, {"a/{b/c,c/b}", "a/b/c", true, true, nil, false, false, false, true, 2, 2}, {"a/{b/c,c/b}", "a/c/b", true, true, nil, false, false, false, true, 2, 2}, {"a/a*{b,c}", "a/abc", true, true, nil, false, false, false, true, 1, 1}, {"{a/{b,c},abc}", "a/b", true, true, nil, false, false, false, true, 3, 3}, {"{a/{b,c},abc}", "a/c", true, true, nil, false, false, false, true, 3, 3}, {"{a/{b,c},abc}", "abc", true, true, nil, false, false, false, true, 3, 3}, {"{a/{b,c},abc}", "a/b/c", false, false, nil, false, false, false, true, 3, 3}, {"{a/ab*}", "a/abc", true, true, nil, false, false, false, true, 1, 1}, {"{a/*}", "a/b", true, true, nil, false, false, false, true, 3, 3}, {"{a/abc}", "a/abc", true, true, nil, false, false, false, true, 1, 1}, {"{a/b,a/c}", "a/c", true, true, nil, false, false, false, true, 2, 2}, {"abc/**", "abc/b", true, true, nil, false, false, false, true, 3, 3}, {"**/abc", "abc", true, true, nil, !onWindows, false, false, true, 2, 2}, {"abc**", "abc/b", false, false, nil, false, false, false, true, 3, 3}, {"**/*.txt", "abc/【test】.txt", true, true, nil, !onWindows, false, false, true, 1, 1}, {"**/【*", "abc/【test】.txt", true, true, nil, !onWindows, false, false, true, 1, 1}, {"**/{a,b}", "a/b", true, true, nil, !onWindows, false, false, true, 5, 5}, // unfortunately, io/fs can't handle this, so neither can Glob =( {"broken-symlink", "broken-symlink", true, true, nil, false, false, true, false, 1, 1}, {"broken-symlink/*", "a", false, false, nil, false, true, true, true, 0, 0}, {"broken*/*", "a", false, false, nil, false, false, true, true, 0, 0}, {"working-symlink/c/*", "working-symlink/c/d", true, true, nil, false, false, true, !onWindows, 1, 1}, {"working-sym*/*", "working-symlink/c", true, true, nil, false, false, true, !onWindows, 1, 1}, {"b/**/f", "b/symlink-dir/f", true, true, nil, false, false, false, !onWindows, 2, 2}, {"*/symlink-dir/*", "b/symlink-dir/f", true, true, nil, !onWindows, false, true, !onWindows, 2, 2}, {"e/**", "e/**", true, true, nil, false, false, false, !onWindows, 11, 6}, {"e/**", "e/*", true, true, nil, false, false, false, !onWindows, 11, 6}, {"e/**", "e/?", true, true, nil, false, false, false, !onWindows, 11, 6}, {"e/**", "e/[", true, true, nil, false, false, false, true, 11, 6}, {"e/**", "e/]", true, true, nil, false, false, false, true, 11, 6}, {"e/**", "e/[]", true, true, nil, false, false, false, true, 11, 6}, {"e/**", "e/{", true, true, nil, false, false, false, true, 11, 6}, {"e/**", "e/}", true, true, nil, false, false, false, true, 11, 6}, {"e/**", "e/\\", true, true, nil, false, false, false, !onWindows, 11, 6}, {"e/*", "e/*", true, true, nil, false, false, true, !onWindows, 10, 5}, {"e/?", "e/?", true, true, nil, false, false, true, !onWindows, 7, 4}, {"e/?", "e/*", true, true, nil, false, false, true, !onWindows, 7, 4}, {"e/?", "e/[", true, true, nil, false, false, true, true, 7, 4}, {"e/?", "e/]", true, true, nil, false, false, true, true, 7, 4}, {"e/?", "e/{", true, true, nil, false, false, true, true, 7, 4}, {"e/?", "e/}", true, true, nil, false, false, true, true, 7, 4}, {"e/\\[", "e/[", true, true, nil, false, false, true, !onWindows, 1, 1}, {"e/[", "e/[", false, false, ErrBadPattern, false, false, true, true, 0, 0}, {"e/]", "e/]", true, true, nil, false, false, true, true, 1, 1}, {"e/\\]", "e/]", true, true, nil, false, false, true, !onWindows, 1, 1}, {"e/\\{", "e/{", true, true, nil, false, false, true, !onWindows, 1, 1}, {"e/\\}", "e/}", true, true, nil, false, false, true, !onWindows, 1, 1}, {"e/[\\*\\?]", "e/*", true, true, nil, false, false, true, !onWindows, 2, 2}, {"e/[\\*\\?]", "e/?", true, true, nil, false, false, true, !onWindows, 2, 2}, {"e/[\\*\\?]", "e/**", false, false, nil, false, false, true, !onWindows, 2, 2}, {"e/[\\*\\?]?", "e/**", true, true, nil, false, false, true, !onWindows, 1, 1}, {"e/{\\*,\\?}", "e/*", true, true, nil, false, false, false, !onWindows, 2, 2}, {"e/{\\*,\\?}", "e/?", true, true, nil, false, false, false, !onWindows, 2, 2}, {"e/\\*", "e/*", true, true, nil, false, false, true, !onWindows, 1, 1}, {"e/\\?", "e/?", true, true, nil, false, false, true, !onWindows, 1, 1}, {"e/\\?", "e/**", false, false, nil, false, false, true, !onWindows, 1, 1}, {"*\\}", "}", true, true, nil, false, false, true, !onWindows, 1, 1}, {"nonexistent-path", "a", false, false, nil, false, true, true, true, 0, 0}, {"nonexistent-path/", "a", false, false, nil, false, true, true, true, 0, 0}, {"nonexistent-path/file", "a", false, false, nil, false, true, true, true, 0, 0}, {"nonexistent-path/*", "a", false, false, nil, false, true, true, true, 0, 0}, {"nonexistent-path/**", "a", false, false, nil, false, true, true, true, 0, 0}, {"nopermission/*", "nopermission/file", true, false, nil, true, false, true, !onWindows, 0, 0}, {"nopermission/dir/", "nopermission/dir", false, false, nil, true, false, true, !onWindows, 0, 0}, {"nopermission/file", "nopermission/file", true, false, nil, true, false, true, !onWindows, 0, 0}, } // Calculate the number of results that we expect // WithFilesOnly at runtime and memoize them here var numResultsFilesOnly []int // Calculate the number of results that we expect // WithNoFollow at runtime and memoize them here var numResultsNoFollow []int // Calculate the number of results that we expect with all // of the options enabled at runtime and memoize them here var numResultsAllOpts []int func TestValidatePattern(t *testing.T) { for idx, tt := range matchTests { testValidatePatternWith(t, idx, tt) } } func testValidatePatternWith(t *testing.T, idx int, tt MatchTest) { defer func() { if r := recover(); r != nil { t.Errorf("#%v. Validate(%#q) panicked: %#v", idx, tt.pattern, r) } }() result := ValidatePattern(tt.pattern) if result != (tt.expectedErr == nil) { t.Errorf("#%v. ValidatePattern(%#q) = %v want %v", idx, tt.pattern, result, !result) } } 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. PathMatch(%#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 TestPathMatchFake(t *testing.T) { // This test fakes that our path separator is `\\` so we can test what it // would be like on Windows - obviously, we don't need to do that if we // actually _are_ on Windows, since TestPathMatch will cover it. if onWindows { return } 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. // On Windows, escaping is disabled. Instead, '\\' is treated as path separator. // So it's not possible to match escaped wild characters. if tt.testOnDisk && !strings.Contains(tt.pattern, "\\") { testPathMatchFakeWith(t, idx, tt) } } } func testPathMatchFakeWith(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 := strings.ReplaceAll(tt.pattern, "/", "\\") testPath := strings.ReplaceAll(tt.testPath, "/", "\\") ok, err := matchWithSeparator(pattern, testPath, '\\', true) if ok != tt.shouldMatch || err != tt.expectedErr { t.Errorf("#%v. PathMatch(%#q, %#q) = %v, %v want %v, %v", idx, pattern, testPath, ok, err, tt.shouldMatch, tt.expectedErr) } } 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) { doGlobTest(t) } func TestGlobWithFailOnIOErrors(t *testing.T) { doGlobTest(t, WithFailOnIOErrors()) } func TestGlobWithFailOnPatternNotExist(t *testing.T) { doGlobTest(t, WithFailOnPatternNotExist()) } func TestGlobWithFilesOnly(t *testing.T) { doGlobTest(t, WithFilesOnly()) } func TestGlobWithNoFollow(t *testing.T) { doGlobTest(t, WithNoFollow()) } func TestGlobWithAllOptions(t *testing.T) { doGlobTest(t, WithFailOnIOErrors(), WithFailOnPatternNotExist(), WithFilesOnly(), WithNoFollow()) } func doGlobTest(t *testing.T, opts ...GlobOption) { glob := newGlob(opts...) fsys := os.DirFS("test") for idx, tt := range matchTests { if tt.testOnDisk { testGlobWith(t, idx, tt, glob, opts, fsys) } } } func testGlobWith(t *testing.T, idx int, tt MatchTest, g *glob, opts []GlobOption, fsys fs.FS) { defer func() { if r := recover(); r != nil { t.Errorf("#%v. Glob(%#q, %#v) panicked: %#v", idx, tt.pattern, g, r) } }() matches, err := Glob(fsys, tt.pattern, opts...) verifyGlobResults(t, idx, "Glob", tt, g, fsys, matches, err) if len(opts) == 0 { testStandardGlob(t, idx, "Glob", tt, fsys, matches, err) } } func TestGlobWalk(t *testing.T) { doGlobWalkTest(t) } func TestGlobWalkWithFailOnIOErrors(t *testing.T) { doGlobWalkTest(t, WithFailOnIOErrors()) } func TestGlobWalkWithFailOnPatternNotExist(t *testing.T) { doGlobWalkTest(t, WithFailOnPatternNotExist()) } func TestGlobWalkWithFilesOnly(t *testing.T) { doGlobWalkTest(t, WithFilesOnly()) } func TestGlobWalkWithNoFollow(t *testing.T) { doGlobWalkTest(t, WithNoFollow()) } func TestGlobWalkWithAllOptions(t *testing.T) { doGlobWalkTest(t, WithFailOnIOErrors(), WithFailOnPatternNotExist(), WithFilesOnly(), WithNoFollow()) } func doGlobWalkTest(t *testing.T, opts ...GlobOption) { glob := newGlob(opts...) fsys := os.DirFS("test") for idx, tt := range matchTests { if tt.testOnDisk { testGlobWalkWith(t, idx, tt, glob, opts, fsys) } } } func testGlobWalkWith(t *testing.T, idx int, tt MatchTest, g *glob, opts []GlobOption, fsys fs.FS) { defer func() { if r := recover(); r != nil { t.Errorf("#%v. Glob(%#q, %#v) panicked: %#v", idx, tt.pattern, opts, r) } }() var matches []string err := GlobWalk(fsys, tt.pattern, func(p string, d fs.DirEntry) error { matches = append(matches, p) return nil }, opts...) verifyGlobResults(t, idx, "GlobWalk", tt, g, fsys, matches, err) if len(opts) == 0 { testStandardGlob(t, idx, "GlobWalk", tt, fsys, matches, err) } } func testStandardGlob(t *testing.T, idx int, fn string, tt MatchTest, fsys fs.FS, matches []string, err error) { if tt.isStandard { stdMatches, stdErr := fs.Glob(fsys, tt.pattern) if !compareSlices(matches, stdMatches) || !compareErrors(err, stdErr) { t.Errorf("#%v. %v(%#q) != fs.Glob(...). Got %#v, %v want %#v, %v", idx, fn, tt.pattern, matches, err, stdMatches, stdErr) } } } func TestFilepathGlob(t *testing.T) { doFilepathGlobTest(t) } func TestFilepathGlobWithFailOnIOErrors(t *testing.T) { doFilepathGlobTest(t, WithFailOnIOErrors()) } func TestFilepathGlobWithFailOnPatternNotExist(t *testing.T) { doFilepathGlobTest(t, WithFailOnPatternNotExist()) } func TestFilepathGlobWithFilesOnly(t *testing.T) { doFilepathGlobTest(t, WithFilesOnly()) } func TestFilepathGlobWithNoFollow(t *testing.T) { doFilepathGlobTest(t, WithNoFollow()) } func doFilepathGlobTest(t *testing.T, opts ...GlobOption) { glob := newGlob(opts...) fsys := os.DirFS("test") // The patterns are relative to the "test" sub-directory. defer func() { os.Chdir("..") }() os.Chdir("test") for idx, tt := range matchTests { // Patterns ending with a slash are treated semantically different by // FilepathGlob vs Glob because FilepathGlob runs filepath.Clean, which // will remove the trailing slash. if tt.testOnDisk && !strings.HasSuffix(tt.pattern, "/") { ttmod := tt ttmod.pattern = filepath.FromSlash(tt.pattern) ttmod.testPath = filepath.FromSlash(tt.testPath) testFilepathGlobWith(t, idx, ttmod, glob, opts, fsys) } } } func testFilepathGlobWith(t *testing.T, idx int, tt MatchTest, g *glob, opts []GlobOption, fsys fs.FS) { defer func() { if r := recover(); r != nil { t.Errorf("#%v. FilepathGlob(%#q, %#v) panicked: %#v", idx, tt.pattern, g, r) } }() matches, err := FilepathGlob(tt.pattern, opts...) verifyGlobResults(t, idx, "FilepathGlob", tt, g, fsys, matches, err) if tt.isStandard && len(opts) == 0 { stdMatches, stdErr := filepath.Glob(tt.pattern) if !compareSlices(matches, stdMatches) || !compareErrors(err, stdErr) { t.Errorf("#%v. FilepathGlob(%#q, %#v) != filepath.Glob(...). Got %#v, %v want %#v, %v", idx, tt.pattern, g, matches, err, stdMatches, stdErr) } } } func verifyGlobResults(t *testing.T, idx int, fn string, tt MatchTest, g *glob, fsys fs.FS, matches []string, err error) { expectedErr := tt.expectedErr if g.failOnPatternNotExist && tt.expectPatternNotExist { expectedErr = ErrPatternNotExist } if g.failOnIOErrors { if tt.expectIOErr { if err == nil { t.Errorf("#%v. %v(%#q, %#v) does not have an error, but should", idx, fn, tt.pattern, g) } return } else if err != nil && err != expectedErr { t.Errorf("#%v. %v(%#q, %#v) has error %v, but should not", idx, fn, tt.pattern, g, err) return } } if !g.failOnPatternNotExist || !tt.expectPatternNotExist { numResults := tt.numResults if onWindows { numResults = tt.winNumResults } if g.filesOnly { if g.noFollow { numResults = numResultsAllOpts[idx] } else { numResults = numResultsFilesOnly[idx] } } else if g.noFollow { numResults = numResultsNoFollow[idx] } if len(matches) != numResults { t.Errorf("#%v. %v(%#q, %#v) = %#v - should have %#v results, got %#v", idx, fn, tt.pattern, g, matches, numResults, len(matches)) } if !g.filesOnly && !g.noFollow && inSlice(tt.testPath, matches) != tt.shouldMatchGlob { if tt.shouldMatchGlob { t.Errorf("#%v. %v(%#q, %#v) = %#v - doesn't contain %v, but should", idx, fn, tt.pattern, g, matches, tt.testPath) } else { t.Errorf("#%v. %v(%#q, %#v) = %#v - contains %v, but shouldn't", idx, fn, tt.pattern, g, matches, tt.testPath) } } } if err != expectedErr { t.Errorf("#%v. %v(%#q, %#v) has error %v, but should be %v", idx, fn, tt.pattern, g, err, expectedErr) } } func TestGlobSorted(t *testing.T) { fsys := os.DirFS("test") expected := []string{"a", "abc", "abcd", "abcde", "abxbbxdbxebxczzx", "abxbbxdbxebxczzy", "axbxcxdxe", "axbxcxdxexxx", "a☺b"} matches, err := Glob(fsys, "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 != expected[idx] { t.Errorf("Glob returned %#v; expected %#v", matches, expected) return } } } func BenchmarkGlob(b *testing.B) { fsys := os.DirFS("test") b.ReportAllocs() for i := 0; i < b.N; i++ { for _, tt := range matchTests { if tt.isStandard && tt.testOnDisk { Glob(fsys, tt.pattern) } } } } func BenchmarkGlobWalk(b *testing.B) { fsys := os.DirFS("test") b.ReportAllocs() for i := 0; i < b.N; i++ { for _, tt := range matchTests { if tt.isStandard && tt.testOnDisk { GlobWalk(fsys, tt.pattern, func(p string, d fs.DirEntry) error { return nil }) } } } } func BenchmarkGoGlob(b *testing.B) { fsys := os.DirFS("test") b.ReportAllocs() for i := 0; i < b.N; i++ { for _, tt := range matchTests { if tt.isStandard && tt.testOnDisk { fs.Glob(fsys, tt.pattern) } } } } 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 buildNumResults() { testLen := len(matchTests) numResultsFilesOnly = make([]int, testLen, testLen) numResultsNoFollow = make([]int, testLen, testLen) numResultsAllOpts = make([]int, testLen, testLen) fsys := os.DirFS("test") g := newGlob() for idx, tt := range matchTests { if tt.testOnDisk { filesOnly := 0 noFollow := 0 allOpts := 0 GlobWalk(fsys, tt.pattern, func(p string, d fs.DirEntry) error { isDir, _ := g.isDir(fsys, "", p, d) if !isDir { filesOnly++ } hasNoFollow := (strings.HasPrefix(tt.pattern, "working-symlink") || !strings.Contains(p, "working-symlink/")) && !strings.Contains(p, "/symlink-dir/") if hasNoFollow { noFollow++ } if hasNoFollow && (!isDir || p == "working-symlink") { allOpts++ } return nil }) numResultsFilesOnly[idx] = filesOnly numResultsNoFollow[idx] = noFollow numResultsAllOpts[idx] = allOpts } } } 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 exists(parts ...string) bool { p := path.Join(parts...) _, err := os.Lstat(p) return err == nil } 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") mkdirp("test", "e") // 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") touch("test", "e", "[") touch("test", "e", "]") touch("test", "e", "{") touch("test", "e", "}") touch("test", "e", "[]") touch("test", "}") if !onWindows { // these files/symlinks won't work on Windows touch("test", "-") touch("test", "]") touch("test", "e", "*") touch("test", "e", "**") touch("test", "e", "****") touch("test", "e", "?") touch("test", "e", "\\") symlink("../axbxcxdxe/", "test/b/symlink-dir") symlink("/tmp/nonexistant-file-20160902155705", "test/broken-symlink") symlink("a/b", "test/working-symlink") if !exists("test", "nopermission") { mkdirp("test", "nopermission", "dir") touch("test", "nopermission", "file") os.Chmod(path.Join("test", "nopermission"), 0) } } // initialize numResultsFilesOnly buildNumResults() os.Exit(m.Run()) } doublestar-4.6.1/doublestar.go0000644000175000017510000000052114514763347015724 0ustar nileshnileshpackage doublestar import ( "errors" "path" ) // ErrBadPattern indicates a pattern was malformed. var ErrBadPattern = path.ErrBadPattern // ErrPatternNotExist indicates that the pattern passed to Glob, GlobWalk, or // FilepathGlob references a path that does not exist. var ErrPatternNotExist = errors.New("pattern does not exist") doublestar-4.6.1/UPGRADING.md0000644000175000017510000000631714514763347015104 0ustar nileshnilesh# Upgrading from v3 to v4 v4 is a complete rewrite with a focus on performance. Additionally, [doublestar] has been updated to use the new [io/fs] package for filesystem access. As a result, it is only supported by [golang] v1.16+. `Match()` and `PathMatch()` mostly did not change, besides big performance improvements. Their API is the same. However, note the following corner cases: * In previous versions of [doublestar], `PathMatch()` could accept patterns that used either platform-specific path separators, or `/`. This was undocumented and didn't match `filepath.Match()`. In v4, both `pattern` and `name` must be using appropriate path separators for the platform. You can use `filepath.FromSlash()` to change `/` to platform-specific separators if you aren't sure. * In previous versions of [doublestar], a pattern such as `path/to/a/**` would _not_ match `path/to/a`. In v4, this pattern _will_ match because if `a` was a directory, `Glob()` would return it. In other words, the following returns true: `Match("path/to/a/**", "path/to/a")` `Glob()` changed from using a [doublestar]-specific filesystem abstraction (the `OS` interface) to the [io/fs] package. As a result, it now takes a `fs.FS` as its first argument. This change has a couple ramifications: * Like `io/fs.Glob`, `pattern` must use a `/` as path separator, even on platforms that use something else. You can use `filepath.ToSlash()` on your patterns if you aren't sure. * Patterns that contain `/./` or `/../` are invalid. The [io/fs] package rejects them, returning an IO error. Since `Glob()` ignores IO errors, it'll end up being silently rejected. You can run `path.Clean()` to ensure they are removed from the pattern. v4 also added a `GlobWalk()` function that is slightly more performant than `Glob()` if you just need to iterate over the results and don't need a string slice. You also get `fs.DirEntry` objects for each result, and can quit early if your callback returns an error. # Upgrading from v2 to v3 v3 introduced using `!` to negate character classes, in addition to `^`. If any of your patterns include a character class that starts with an exclamation mark (ie, `[!...]`), you'll need to update the pattern to escape or move the exclamation mark. Note that, like the caret (`^`), it only negates the character class if it is the first character in the character class. # 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], [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. [afero]: https://github.com/spf13/afero [doublestar]: https://github.com/bmatcuk/doublestar [go-billy]: https://github.com/src-d/go-billy [golang]: http://golang.org/ [io/fs]: https://golang.org/pkg/io/fs/ doublestar-4.6.1/README.md0000644000175000017510000003676014514763347014526 0ustar nileshnilesh# 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/v4) [![Release](https://img.shields.io/github/release/bmatcuk/doublestar.svg?branch=master)](https://github.com/bmatcuk/doublestar/releases) [![Build Status](https://github.com/bmatcuk/doublestar/actions/workflows/test.yml/badge.svg)](https://github.com/bmatcuk/doublestar/actions) [![codecov.io](https://img.shields.io/codecov/c/github/bmatcuk/doublestar.svg?branch=master)](https://codecov.io/github/bmatcuk/doublestar?branch=master) [![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/bmatcuk) ## About #### [Upgrading?](UPGRADING.md) **doublestar** is a [golang] 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. v4 is a complete rewrite with a focus on performance. Additionally, [doublestar] has been updated to use the new [io/fs] package for filesystem access. As a result, it is only supported by [golang] v1.16+. ## Installation **doublestar** can be installed via `go get`: ```bash go get github.com/bmatcuk/doublestar/v4 ``` To use it in your code, you must import it: ```go import "github.com/bmatcuk/doublestar/v4" ``` ## Usage ### ErrBadPattern ```go doublestar.ErrBadPattern ``` Returned by various functions to report that the pattern is malformed. At the moment, this value is equal to `path.ErrBadPattern`, but, for portability, this equivalence should probably not be relied upon. ### Match ```go func Match(pattern, name string) (bool, error) ``` Match returns true if `name` matches the file name `pattern` ([see "patterns"]). `name` and `pattern` are split on forward slash (`/`) characters and may be relative or absolute. Match requires pattern to match all of name, not just a substring. 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 `PathMatch()`. Alternatively, you can run `filepath.ToSlash()` on both pattern and name and then use this function. Note: users should _not_ count on the returned error, `doublestar.ErrBadPattern`, being equal to `path.ErrBadPattern`. ### PathMatch ```go func PathMatch(pattern, name string) (bool, error) ``` PathMatch returns true if `name` matches the file name `pattern` ([see "patterns"]). The difference between Match and PathMatch is that PathMatch will automatically use your system's path separator to split `name` and `pattern`. On systems where the path separator is `'\'`, escaping will be disabled. Note: this is meant as a drop-in replacement for `filepath.Match()`. It assumes that both `pattern` and `name` are using the system's path separator. If you can't be sure of that, use `filepath.ToSlash()` on both `pattern` and `name`, and then use the `Match()` function instead. ### GlobOption Options that may be passed to `Glob`, `GlobWalk`, or `FilepathGlob`. Any number of options may be passed to these functions, and in any order, as the last argument(s). ```go WithFailOnIOErrors() ``` If passed, doublestar will abort and return IO errors when encountered. Note that if the glob pattern references a path that does not exist (such as `nonexistent/path/*`), this is _not_ considered an IO error: it is considered a pattern with no matches. ```go WithFailOnPatternNotExist() ``` If passed, doublestar will abort and return `doublestar.ErrPatternNotExist` if the pattern references a path that does not exist before any meta characters such as `nonexistent/path/*`. Note that alts (ie, `{...}`) are expanded before this check. In other words, a pattern such as `{a,b}/*` may fail if either `a` or `b` do not exist but `*/{a,b}` will never fail because the star may match nothing. ```go WithFilesOnly() ``` If passed, doublestar will only return "files" from `Glob`, `GlobWalk`, or `FilepathGlob`. In this context, "files" are anything that is not a directory or a symlink to a directory. Note: if combined with the WithNoFollow option, symlinks to directories _will_ be included in the result since no attempt is made to follow the symlink. ```go WithNoFollow() ``` If passed, doublestar will not follow symlinks while traversing the filesystem. However, due to io/fs's _very_ poor support for querying the filesystem about symlinks, there's a caveat here: if part of the pattern before any meta characters contains a reference to a symlink, it will be followed. For example, a pattern such as `path/to/symlink/*` will be followed assuming it is a valid symlink to a directory. However, from this same example, a pattern such as `path/to/**` will not traverse the `symlink`, nor would `path/*/symlink/*` Note: if combined with the WithFilesOnly option, symlinks to directories _will_ be included in the result since no attempt is made to follow the symlink. ### Glob ```go func Glob(fsys fs.FS, pattern string, opts ...GlobOption) ([]string, error) ``` Glob returns the names of all files matching pattern or nil if there is no matching file. The syntax of patterns is the same as in `Match()`. The pattern may describe hierarchical names such as `usr/*/bin/ed`. Glob ignores file system errors such as I/O errors reading directories by default. The only possible returned error is `ErrBadPattern`, reporting that the pattern is malformed. To enable aborting on I/O errors, the `WithFailOnIOErrors` option can be passed. Note: this is meant as a drop-in replacement for `io/fs.Glob()`. Like `io/fs.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 `Glob()`. Like `io/fs.Glob()`, patterns containing `/./`, `/../`, or starting with `/` will return no results and no errors. This seems to be a [conscious decision](https://github.com/golang/go/issues/44092#issuecomment-774132549), even if counter-intuitive. You can use [SplitPattern] to divide a pattern into a base path (to initialize an `FS` object) and pattern. Note: users should _not_ count on the returned error, `doublestar.ErrBadPattern`, being equal to `path.ErrBadPattern`. ### GlobWalk ```go type GlobWalkFunc func(path string, d fs.DirEntry) error func GlobWalk(fsys fs.FS, pattern string, fn GlobWalkFunc, opts ...GlobOption) 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`. ### FilepathGlob ```go func FilepathGlob(pattern string, opts ...GlobOption) (matches []string, err error) ``` FilepathGlob 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. FilepathGlob ignores file system errors such as I/O errors reading directories by default. The only possible returned error is `ErrBadPattern`, reporting that the pattern is malformed. To enable aborting on I/O errors, the `WithFailOnIOErrors` option can be passed. Note: FilepathGlob is a convenience function that is meant as a drop-in replacement for `path/filepath.Glob()` for users who don't need the complication of io/fs. Basically, it: * Runs `filepath.Clean()` and `ToSlash()` on the pattern * Runs `SplitPattern()` to get a base path and a pattern to Glob * Creates an FS object from the base path and `Glob()s` on the pattern * Joins the base path with all of the matches from `Glob()` Returned paths will use the system's path separator, just like `filepath.Glob()`. Note: the returned error `doublestar.ErrBadPattern` is not equal to `filepath.ErrBadPattern`. ### SplitPattern ```go func SplitPattern(p string) (base, pattern string) ``` SplitPattern is a utility function. Given a pattern, SplitPattern will return two strings: the first string is everything up to the last slash (`/`) that appears _before_ any unescaped "meta" characters (ie, `*?[{`). The second string is everything after that slash. For example, given the pattern: ``` ../../path/to/meta*/** ^----------- split here ``` SplitPattern returns "../../path/to" and "meta*/**". This is useful for initializing os.DirFS() to call Glob() because Glob() will silently fail if your pattern includes `/./` or `/../`. For example: ```go base, pattern := SplitPattern("../../path/to/meta*/**") fsys := os.DirFS(base) matches, err := Glob(fsys, pattern) ``` If SplitPattern cannot find somewhere to split the pattern (for example, `meta*/**`), it will return "." and the unaltered pattern (`meta*/**` in this example). Of course, it is your responsibility to decide if the returned base path is "safe" in the context of your application. Perhaps you could use Match() to validate against a list of approved base directories? ### ValidatePattern ```go func ValidatePattern(s string) bool ``` Validate a pattern. Patterns are validated while they run in Match(), PathMatch(), and Glob(), so, you normally wouldn't need to call this. However, there are cases where this might be useful: for example, if your program allows a user to enter a pattern that you'll run at a later time, you might want to validate it. ValidatePattern assumes your pattern uses '/' as the path separator. ### ValidatePathPattern ```go func ValidatePathPattern(s string) bool ``` Like ValidatePattern, only uses your OS path separator. In other words, use ValidatePattern if you would normally use Match() or Glob(). Use ValidatePathPattern if you would normally use PathMatch(). Keep in mind, Glob() requires '/' separators, even if your OS uses something else. ### Patterns **doublestar** supports the following special terms in the patterns: Special Terms | Meaning ------------- | ------- `*` | matches any sequence of non-path-separators `/**/` | matches zero or more directories `?` | matches any single non-path-separator character `[class]` | matches any single non-path-separator character against a class of characters ([see "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 doublestar (`**`) should appear surrounded by path separators such as `/**/`. 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 `[!class]` | same as `^`: negates the class ## Performance ``` goos: darwin goarch: amd64 pkg: github.com/bmatcuk/doublestar/v4 cpu: Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz BenchmarkMatch-8 285639 3868 ns/op 0 B/op 0 allocs/op BenchmarkGoMatch-8 286945 3726 ns/op 0 B/op 0 allocs/op BenchmarkPathMatch-8 320511 3493 ns/op 0 B/op 0 allocs/op BenchmarkGoPathMatch-8 304236 3434 ns/op 0 B/op 0 allocs/op BenchmarkGlob-8 466 2501123 ns/op 190225 B/op 2849 allocs/op BenchmarkGlobWalk-8 476 2536293 ns/op 184017 B/op 2750 allocs/op BenchmarkGoGlob-8 463 2574836 ns/op 194249 B/op 2929 allocs/op ``` These benchmarks (in `doublestar_test.go`) compare Match() to path.Match(), PathMath() to filepath.Match(), and Glob() + GlobWalk() to io/fs.Glob(). They only run patterns that the standard go packages can understand as well (so, no `{alts}` or `**`) for a fair comparison. Of course, alts and doublestars will be less performant than the other pattern meta characters. Alts are essentially like running multiple patterns, the number of which can get large if your pattern has alts nested inside alts. This affects both matching (ie, Match()) and globbing (Glob()). `**` performance in matching is actually pretty similar to a regular `*`, but can cause a large number of reads when globbing as it will need to recursively traverse your filesystem. ## Sponsors I started this project in 2014 in my spare time and have been maintaining it ever since. In that time, it has grown into one of the most popular globbing libraries in the Go ecosystem. So, if **doublestar** is a useful library in your project, consider [sponsoring] my work! I'd really appreciate it! Thanks for sponsoring me! ## License [MIT License](LICENSE) [SplitPattern]: #splitpattern [doublestar]: https://github.com/bmatcuk/doublestar [golang]: http://golang.org/ [io/fs]: https://pkg.go.dev/io/fs [see "character classes"]: #character-classes [see "patterns"]: #patterns [sponsoring]: https://github.com/sponsors/bmatcuk doublestar-4.6.1/LICENSE0000644000175000017510000000206614514763347014244 0ustar nileshnileshThe 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-4.6.1/.gitignore0000644000175000017510000000046714514763347015232 0ustar nileshnilesh# 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-4.6.1/.codecov.yml0000644000175000017510000000021714514763347015456 0ustar nileshnileshcoverage: status: project: default: threshold: 1% patch: default: target: 70% ignore: - globoptions.go