Skip to content

Commit

Permalink
Merge pull request #81 from bmatcuk/withfilesonly
Browse files Browse the repository at this point in the history
Added WithFilesOnly option
  • Loading branch information
bmatcuk authored Jan 4, 2023
2 parents 65c0f86 + 016ef08 commit 3c85a19
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 47 deletions.
2 changes: 2 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ coverage:
patch:
default:
target: 70%
ignore:
- globoptions.go
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ 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.

### Glob

```go
Expand Down
49 changes: 46 additions & 3 deletions doublestar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ var matchTests = []MatchTest{
{"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

func TestValidatePattern(t *testing.T) {
for idx, tt := range matchTests {
testValidatePatternWith(t, idx, tt)
Expand Down Expand Up @@ -366,8 +370,12 @@ func TestGlobWithFailOnPatternNotExist(t *testing.T) {
doGlobTest(t, WithFailOnPatternNotExist())
}

func TestGlobWithFilesOnly(t *testing.T) {
doGlobTest(t, WithFilesOnly())
}

func TestGlobWithAllOptions(t *testing.T) {
doGlobTest(t, WithFailOnIOErrors(), WithFailOnPatternNotExist())
doGlobTest(t, WithFailOnIOErrors(), WithFailOnPatternNotExist(), WithFilesOnly())
}

func doGlobTest(t *testing.T, opts ...GlobOption) {
Expand Down Expand Up @@ -406,8 +414,12 @@ func TestGlobWalkWithFailOnPatternNotExist(t *testing.T) {
doGlobWalkTest(t, WithFailOnPatternNotExist())
}

func TestGlobWalkWithFilesOnly(t *testing.T) {
doGlobWalkTest(t, WithFilesOnly())
}

func TestGlobWalkWithAllOptions(t *testing.T) {
doGlobWalkTest(t, WithFailOnIOErrors(), WithFailOnPatternNotExist())
doGlobWalkTest(t, WithFailOnIOErrors(), WithFailOnPatternNotExist(), WithFilesOnly())
}

func doGlobWalkTest(t *testing.T, opts ...GlobOption) {
Expand Down Expand Up @@ -459,6 +471,10 @@ func TestFilepathGlobWithFailOnPatternNotExist(t *testing.T) {
doFilepathGlobTest(t, WithFailOnPatternNotExist())
}

func TestFilepathGlobWithFilesOnly(t *testing.T) {
doFilepathGlobTest(t, WithFilesOnly())
}

func doFilepathGlobTest(t *testing.T, opts ...GlobOption) {
glob := newGlob(opts...)
fsys := os.DirFS("test")
Expand Down Expand Up @@ -520,11 +536,14 @@ func verifyGlobResults(t *testing.T, idx int, fn string, tt MatchTest, g *glob,
if onWindows {
numResults = tt.winNumResults
}
if g.filesOnly {
numResults = numResultsFilesOnly[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 inSlice(tt.testPath, matches) != tt.shouldMatchGlob {
if !g.filesOnly && 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 {
Expand Down Expand Up @@ -637,6 +656,27 @@ func compareSlices(a, b []string) bool {
return len(diff) == 0
}

func buildNumResultsFilesOnly() {
testLen := len(matchTests)
numResultsFilesOnly = make([]int, testLen, testLen)

fsys := os.DirFS("test")
g := newGlob()
for idx, tt := range matchTests {
if tt.testOnDisk {
count := 0
GlobWalk(fsys, tt.pattern, func(p string, d fs.DirEntry) error {
isDir, _ := g.isDir(fsys, "", p, d)
if !isDir {
count++
}
return nil
})
numResultsFilesOnly[idx] = count
}
}
}

func mkdirp(parts ...string) {
dirs := path.Join(parts...)
err := os.MkdirAll(dirs, 0755)
Expand Down Expand Up @@ -729,5 +769,8 @@ func TestMain(m *testing.M) {
}
}

// initialize numResultsFilesOnly
buildNumResultsFilesOnly()

os.Exit(m.Run())
}
38 changes: 24 additions & 14 deletions glob.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,12 @@ func (g *glob) doGlob(fsys fs.FS, pattern string, m []string, firstSegment, befo
// pattern exist?
// The pattern may contain escaped wildcard characters for an exact path match.
path := unescapeMeta(pattern)
_, pathExists, pathErr := g.exists(fsys, path, beforeMeta)
pathInfo, pathExists, pathErr := g.exists(fsys, path, beforeMeta)
if pathErr != nil {
return nil, pathErr
}

if pathExists {
if pathExists && (!firstSegment || !g.filesOnly || !pathInfo.IsDir()) {
matches = append(matches, path)
}

Expand Down Expand Up @@ -194,15 +194,17 @@ func (g *glob) globDir(fsys fs.FS, dir, pattern string, matches []string, canMat
m = matches

if pattern == "" {
// 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)
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
}
Expand Down Expand Up @@ -230,11 +232,16 @@ func (g *glob) globDir(fsys fs.FS, dir, pattern string, matches []string, canMat
}
if matched {
matched = canMatchFiles
if !matched {
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))
Expand All @@ -255,8 +262,11 @@ func (g *glob) globDoubleStar(fsys fs.FS, dir string, matches []string, canMatch
}
}

// `**` can match *this* dir, so add it
matches = append(matches, dir)
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)
Expand Down
45 changes: 38 additions & 7 deletions globoptions.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package doublestar

import "strings"

// glob is an internal type to store options during globbing.
type glob struct {
failOnIOErrors bool
failOnPatternNotExist bool
filesOnly bool
}

// GlobOption represents a setting that can be passed to Glob, GlobWalk, and
Expand Down Expand Up @@ -45,6 +48,16 @@ func WithFailOnPatternNotExist() GlobOption {
}
}

// 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.
//
func WithFilesOnly() GlobOption {
return func(g *glob) {
g.filesOnly = 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.
Expand All @@ -69,13 +82,31 @@ func (g *glob) handlePatternNotExist(canFail bool) error {

// Format options for debugging/testing purposes
func (g *glob) GoString() string {
if g.failOnIOErrors {
if g.failOnPatternNotExist {
return "opts: WithFailOnIOErrors, WithFailOnPatternNotExist"
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(", ")
}
return "opts: WithFailOnIOErrors"
} else if g.failOnPatternNotExist {
return "opts: WithFailOnPatternNotExist"
b.WriteString("WithFilesOnly")
hasOpts = true
}

if !hasOpts {
b.WriteString("nil")
}
return "opts: nil"
return b.String()
}
57 changes: 34 additions & 23 deletions globwalk.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (g *glob) doGlobWalk(fsys fs.FS, pattern string, firstSegment, beforeMeta b
// 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 {
if pathExists && (!firstSegment || !g.filesOnly || !info.IsDir()) {
err = fn(path, dirEntryFromFileInfo(info))
if err == SkipDir {
err = nil
Expand Down Expand Up @@ -241,17 +241,19 @@ func (g *glob) doGlobAltsWalk(fsys fs.FS, d, pattern string, startIdx, openingId

func (g *glob) globDirWalk(fsys fs.FS, dir, pattern string, canMatchFiles, beforeMeta bool, fn GlobWalkFunc) (e error) {
if pattern == "" {
// 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
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
Expand All @@ -266,11 +268,13 @@ func (g *glob) globDirWalk(fsys fs.FS, dir, pattern string, canMatchFiles, befor
if !dirExists || !info.IsDir() {
return nil
}
if e = fn(dir, dirEntryFromFileInfo(info)); e != nil {
if e == SkipDir {
e = nil
if !canMatchFiles || !g.filesOnly {
if e = fn(dir, dirEntryFromFileInfo(info)); e != nil {
if e == SkipDir {
e = nil
}
return
}
return
}
return g.globDoubleStarWalk(fsys, dir, canMatchFiles, fn)
}
Expand All @@ -292,11 +296,16 @@ func (g *glob) globDirWalk(fsys fs.FS, dir, pattern string, canMatchFiles, befor
}
if matched {
matched = canMatchFiles
if !matched {
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 {
Expand Down Expand Up @@ -325,7 +334,6 @@ func (g *glob) globDoubleStarWalk(fsys fs.FS, dir string, canMatchFiles bool, fn
return g.forwardErrIfFailOnIOErrors(err)
}

// `**` can match *this* dir, so add it
for _, info := range dirs {
name := info.Name()
isDir, err := g.isDir(fsys, dir, name, info)
Expand All @@ -335,12 +343,15 @@ func (g *glob) globDoubleStarWalk(fsys fs.FS, dir string, canMatchFiles bool, fn

if isDir {
p := path.Join(dir, name)
if e = fn(p, info); e != nil {
if e == SkipDir {
e = nil
continue
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
}
return
}
if e = g.globDoubleStarWalk(fsys, p, canMatchFiles, fn); e != nil {
return
Expand Down

0 comments on commit 3c85a19

Please sign in to comment.