Skip to content

Commit

Permalink
Merge pull request #82 from bmatcuk/withnofollow
Browse files Browse the repository at this point in the history
fixes #72 adds WithNoFollow option with caveat
  • Loading branch information
bmatcuk authored Jan 5, 2023
2 parents 3c85a19 + c5b3a48 commit 465a339
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 16 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,24 @@ 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
Expand Down
66 changes: 55 additions & 11 deletions doublestar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ var matchTests = []MatchTest{
{"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},
Expand Down Expand Up @@ -188,10 +189,18 @@ 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
// 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)
Expand Down Expand Up @@ -374,8 +383,12 @@ 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())
doGlobTest(t, WithFailOnIOErrors(), WithFailOnPatternNotExist(), WithFilesOnly(), WithNoFollow())
}

func doGlobTest(t *testing.T, opts ...GlobOption) {
Expand Down Expand Up @@ -418,8 +431,12 @@ 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())
doGlobWalkTest(t, WithFailOnIOErrors(), WithFailOnPatternNotExist(), WithFilesOnly(), WithNoFollow())
}

func doGlobWalkTest(t *testing.T, opts ...GlobOption) {
Expand Down Expand Up @@ -475,6 +492,10 @@ 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")
Expand Down Expand Up @@ -537,13 +558,19 @@ func verifyGlobResults(t *testing.T, idx int, fn string, tt MatchTest, g *glob,
numResults = tt.winNumResults
}
if g.filesOnly {
numResults = numResultsFilesOnly[idx]
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 && inSlice(tt.testPath, matches) != tt.shouldMatchGlob {
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 {
Expand Down Expand Up @@ -656,23 +683,40 @@ func compareSlices(a, b []string) bool {
return len(diff) == 0
}

func buildNumResultsFilesOnly() {
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 {
count := 0
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 {
count++
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] = count

numResultsFilesOnly[idx] = filesOnly
numResultsNoFollow[idx] = noFollow
numResultsAllOpts[idx] = allOpts
}
}
}
Expand Down Expand Up @@ -770,7 +814,7 @@ func TestMain(m *testing.M) {
}

// initialize numResultsFilesOnly
buildNumResultsFilesOnly()
buildNumResults()

os.Exit(m.Run())
}
2 changes: 1 addition & 1 deletion glob.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ func (g *glob) isPathDir(fsys fs.FS, name string, beforeMeta bool) (fs.FileInfo,
// 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 (info.Type() & fs.ModeSymlink) > 0 {
if !g.noFollow && (info.Type()&fs.ModeSymlink) > 0 {
p := name
if dir != "" {
p = path.Join(dir, name)
Expand Down
38 changes: 35 additions & 3 deletions globoptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type glob struct {
failOnIOErrors bool
failOnPatternNotExist bool
filesOnly bool
noFollow bool
}

// GlobOption represents a setting that can be passed to Glob, GlobWalk, and
Expand Down Expand Up @@ -52,12 +53,36 @@ func WithFailOnPatternNotExist() GlobOption {
// 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.
Expand Down Expand Up @@ -86,24 +111,31 @@ func (g *glob) GoString() string {
b.WriteString("opts: ")

hasOpts := false
if (g.failOnIOErrors) {
if g.failOnIOErrors {
b.WriteString("WithFailOnIOErrors")
hasOpts = true
}
if (g.failOnPatternNotExist) {
if g.failOnPatternNotExist {
if hasOpts {
b.WriteString(", ")
}
b.WriteString("WithFailOnPatternNotExist")
hasOpts = true
}
if (g.filesOnly) {
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")
Expand Down
2 changes: 1 addition & 1 deletion utils_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package doublestar

import (
"testing"
"path/filepath"
"testing"
)

var filepathGlobTests = []string{
Expand Down

0 comments on commit 465a339

Please sign in to comment.