Skip to content

Commit df2e03f

Browse files
committed
feat: add WithNoHidden option to skip hidden files
- Add WithNoHidden() option following traditional shell glob behavior - ** expansion skips directories starting with . - * at start of path segment skips names starting with . - Explicit patterns like .* or .config/** still work - Integrate tests into doGlobTest framework
1 parent 3dc8306 commit df2e03f

5 files changed

Lines changed: 105 additions & 7 deletions

File tree

doublestar_test.go

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ var matchTests = []MatchTest{
3636
{"/*", "/debug/", false, false, false, nil, false, false, true, false, 0, 0},
3737
{"/*", "//", false, false, false, nil, false, false, true, false, 0, 0},
3838
{"abc", "abc", true, true, false, nil, false, false, true, true, 1, 1},
39-
{"*", "abc", true, true, false, nil, false, false, true, true, 25, 20},
39+
{"*", "abc", true, true, false, nil, false, false, true, true, 29, 24},
4040
{"*c", "abc", true, true, false, nil, false, false, true, true, 2, 2},
4141
{"*/", "a/", true, true, false, nil, false, false, true, false, 0, 0},
4242
{"a*", "a", true, true, false, nil, false, false, true, true, 9, 9},
@@ -64,8 +64,8 @@ var matchTests = []MatchTest{
6464
{"a[!a]b", "a☺b", true, true, false, nil, false, false, false, true, 1, 1},
6565
{"a???b", "a☺b", false, false, false, nil, false, false, true, true, 0, 0},
6666
{"a[^a][^a][^a]b", "a☺b", false, false, false, nil, false, false, true, true, 0, 0},
67-
{"[a-ζ]*", "α", true, true, false, nil, false, false, true, true, 22, 19},
68-
{"*[a-ζ]", "A", false, false, false, nil, false, false, true, true, 22, 19},
67+
{"[a-ζ]*", "α", true, true, false, nil, false, false, true, true, 23, 20},
68+
{"*[a-ζ]", "A", false, false, false, nil, false, false, true, true, 26, 23},
6969
{"a?b", "a/b", false, false, false, nil, false, false, true, true, 1, 1},
7070
{"a*b", "a/b", false, false, false, nil, false, false, true, true, 1, 1},
7171
{"[\\]a]", "]", true, true, false, nil, false, false, true, !onWindows, 2, 2},
@@ -203,6 +203,11 @@ var matchTests = []MatchTest{
203203
{"nopermission/*", "nopermission/file", true, false, false, nil, true, false, true, !onWindows, 0, 0},
204204
{"nopermission/dir/", "nopermission/dir", false, false, false, nil, true, false, true, !onWindows, 0, 0},
205205
{"nopermission/file", "nopermission/file", true, false, false, nil, true, false, true, !onWindows, 0, 0},
206+
{".*", ".hidden_file", true, true, false, nil, false, false, false, true, 3, 3},
207+
{".hidden_dir/**", ".hidden_dir", true, true, false, nil, false, false, false, true, 2, 2},
208+
{".hidden_dir/*", ".hidden_dir/.nested_hidden", true, true, false, nil, false, false, false, true, 1, 1},
209+
{".another_hidden/file", ".another_hidden/file", true, true, false, nil, false, false, false, true, 1, 1},
210+
{"foo/**/bar", "foo/visible/bar", true, true, false, nil, false, false, false, true, 4, 4},
206211
}
207212

208213
// True if the file system supports case-sensitive filenames
@@ -220,6 +225,10 @@ var numResultsNoFollow []int
220225
// of the options enabled at runtime and memoize them here
221226
var numResultsAllOpts []int
222227

228+
// Calculate the number of results that we expect
229+
// WithNoHidden at runtime and memoize them here
230+
var numResultsNoHidden []int
231+
223232
func TestValidatePattern(t *testing.T) {
224233
for idx, tt := range matchTests {
225234
testValidatePatternWith(t, idx, tt)
@@ -462,6 +471,10 @@ func TestGlobWithNoFollow(t *testing.T) {
462471
doGlobTest(t, WithNoFollow())
463472
}
464473

474+
func TestGlobWithNoHidden(t *testing.T) {
475+
doGlobTest(t, WithNoHidden())
476+
}
477+
465478
func TestGlobWithAllOptions(t *testing.T) {
466479
doGlobTest(t, WithCaseInsensitive(), WithFailOnIOErrors(), WithFailOnPatternNotExist(), WithFilesOnly(), WithNoFollow())
467480
}
@@ -510,6 +523,10 @@ func TestGlobWalkWithNoFollow(t *testing.T) {
510523
doGlobWalkTest(t, WithNoFollow())
511524
}
512525

526+
func TestGlobWalkWithNoHidden(t *testing.T) {
527+
doGlobWalkTest(t, WithNoHidden())
528+
}
529+
513530
func TestGlobWalkWithAllOptions(t *testing.T) {
514531
doGlobWalkTest(t, WithFailOnIOErrors(), WithFailOnPatternNotExist(), WithFilesOnly(), WithNoFollow())
515532
}
@@ -571,6 +588,10 @@ func TestFilepathGlobWithNoFollow(t *testing.T) {
571588
doFilepathGlobTest(t, WithNoFollow())
572589
}
573590

591+
func TestFilepathGlobWithNoHidden(t *testing.T) {
592+
doFilepathGlobTest(t, WithNoHidden())
593+
}
594+
574595
func doFilepathGlobTest(t *testing.T, opts ...GlobOption) {
575596
glob := newGlob(opts...)
576597
fsys := os.DirFS("test")
@@ -635,7 +656,9 @@ func verifyGlobResults(t *testing.T, idx int, fn string, tt MatchTest, g *glob,
635656
if onWindows {
636657
numResults = tt.winNumResults
637658
}
638-
if g.filesOnly {
659+
if g.noHidden {
660+
numResults = numResultsNoHidden[idx]
661+
} else if g.filesOnly {
639662
if g.noFollow {
640663
numResults = numResultsAllOpts[idx]
641664
} else {
@@ -651,7 +674,8 @@ func verifyGlobResults(t *testing.T, idx int, fn string, tt MatchTest, g *glob,
651674
if len(matches) != numResults {
652675
t.Errorf("#%v. %v(%#q, %#v) = %#v - should have %#v results, got %#v", idx, fn, tt.pattern, g, matches, numResults, len(matches))
653676
}
654-
if !g.filesOnly && !g.noFollow && inSlice(tt.testPath, matches) != tt.shouldMatchGlob {
677+
// Skip testPath check for noHidden since the match semantics are different
678+
if !g.filesOnly && !g.noFollow && !g.noHidden && inSlice(tt.testPath, matches) != tt.shouldMatchGlob {
655679
if tt.shouldMatchGlob {
656680
t.Errorf("#%v. %v(%#q, %#v) = %#v - doesn't contain %v, but should", idx, fn, tt.pattern, g, matches, tt.testPath)
657681
} else {
@@ -769,6 +793,7 @@ func buildNumResults() {
769793
numResultsFilesOnly = make([]int, testLen, testLen)
770794
numResultsNoFollow = make([]int, testLen, testLen)
771795
numResultsAllOpts = make([]int, testLen, testLen)
796+
numResultsNoHidden = make([]int, testLen, testLen)
772797

773798
fsys := os.DirFS("test")
774799
g := newGlob()
@@ -798,6 +823,14 @@ func buildNumResults() {
798823
numResultsFilesOnly[idx] = filesOnly
799824
numResultsNoFollow[idx] = noFollow
800825
numResultsAllOpts[idx] = allOpts
826+
827+
// Compute noHidden results by actually running with WithNoHidden
828+
noHidden := 0
829+
GlobWalk(fsys, tt.pattern, func(p string, d fs.DirEntry) error {
830+
noHidden++
831+
return nil
832+
}, WithNoHidden())
833+
numResultsNoHidden[idx] = noHidden
801834
}
802835
}
803836
}
@@ -882,6 +915,18 @@ func TestMain(m *testing.M) {
882915

883916
touch("test", "}")
884917

918+
mkdirp("test", ".hidden_dir")
919+
mkdirp("test", ".another_hidden")
920+
mkdirp("test", "foo", ".hidden", "deep")
921+
mkdirp("test", "foo", "visible")
922+
touch("test", ".hidden_file")
923+
touch("test", ".hidden_dir", ".nested_hidden")
924+
touch("test", ".another_hidden", "file")
925+
touch("test", "foo", ".hidden", "bar")
926+
touch("test", "foo", ".hidden", "deep", "bar")
927+
touch("test", "foo", "visible", "bar")
928+
touch("test", "foo", "bar")
929+
885930
if !onWindows {
886931
// these files/symlinks won't work on Windows
887932
touch("test", "-")

glob.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,15 @@ func (g *glob) globDir(fsys fs.FS, dir, pattern string, matches []string, canMat
223223
}
224224

225225
var matched bool
226+
patternStartsWithStar := len(pattern) > 0 && pattern[0] == '*'
226227
for _, info := range dirs {
227228
name := info.Name()
229+
230+
// Skip hidden files when noHidden is set and pattern starts with *
231+
if g.noHidden && patternStartsWithStar && len(name) > 0 && name[0] == '.' {
232+
continue
233+
}
234+
228235
matched, e = matchWithSeparator(pattern, name, '/', false, g.caseInsensitive)
229236
if e != nil {
230237
return
@@ -268,6 +275,12 @@ func (g *glob) globDoubleStar(fsys fs.FS, dir string, matches []string, canMatch
268275

269276
for _, info := range dirs {
270277
name := info.Name()
278+
279+
// Skip hidden files/directories when noHidden is set
280+
if g.noHidden && len(name) > 0 && name[0] == '.' {
281+
continue
282+
}
283+
271284
isDir, err := g.isDir(fsys, dir, name, info)
272285
if err != nil {
273286
return nil, err

globoptions.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import "strings"
44

55
// glob is an internal type to store options during globbing.
66
type glob struct {
7-
caseInsensitive bool
7+
caseInsensitive bool
88
failOnIOErrors bool
99
failOnPatternNotExist bool
1010
filesOnly bool
1111
noFollow bool
12+
noHidden bool
1213
}
1314

1415
// GlobOption represents a setting that can be passed to Glob, GlobWalk, and
@@ -90,6 +91,25 @@ func WithNoFollow() GlobOption {
9091
}
9192
}
9293

94+
// WithNoHidden is an option that can be passed to Glob, GlobWalk, or
95+
// FilepathGlob. If passed, doublestar will not match hidden files and
96+
// directories (those starting with a dot) when using wildcards. This follows
97+
// traditional shell glob behavior where `*` does not match dotfiles by default.
98+
//
99+
// Hidden files can still be matched by explicitly including them in the
100+
// pattern. For example, `.*` will match hidden files, and `.config/**` will
101+
// match files inside the .config directory.
102+
//
103+
// The rule is:
104+
// - For `**`: do not descend into directories starting with `.`
105+
// - For `*`: if the `*` is at the start of a path segment, do not match
106+
// names starting with `.`
107+
func WithNoHidden() GlobOption {
108+
return func(g *glob) {
109+
g.noHidden = true
110+
}
111+
}
112+
93113
// forwardErrIfFailOnIOErrors is used to wrap the return values of I/O
94114
// functions. When failOnIOErrors is enabled, it will return err; otherwise, it
95115
// always returns nil.
@@ -148,6 +168,13 @@ func (g *glob) GoString() string {
148168
b.WriteString("WithNoFollow")
149169
hasOpts = true
150170
}
171+
if g.noHidden {
172+
if hasOpts {
173+
b.WriteString(", ")
174+
}
175+
b.WriteString("WithNoHidden")
176+
hasOpts = true
177+
}
151178

152179
if !hasOpts {
153180
b.WriteString("nil")

globwalk.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,15 @@ func (g *glob) globDirWalk(fsys fs.FS, dir, pattern string, canMatchFiles, befor
287287
}
288288

289289
var matched bool
290+
patternStartsWithStar := len(pattern) > 0 && pattern[0] == '*'
290291
for _, info := range dirs {
291292
name := info.Name()
293+
294+
// Skip hidden files when noHidden is set and pattern starts with *
295+
if g.noHidden && patternStartsWithStar && len(name) > 0 && name[0] == '.' {
296+
continue
297+
}
298+
292299
matched, e = matchWithSeparator(pattern, name, '/', false, g.caseInsensitive)
293300
if e != nil {
294301
return
@@ -335,6 +342,12 @@ func (g *glob) globDoubleStarWalk(fsys fs.FS, dir string, canMatchFiles bool, fn
335342

336343
for _, info := range dirs {
337344
name := info.Name()
345+
346+
// Skip hidden files/directories when noHidden is set
347+
if g.noHidden && len(name) > 0 && name[0] == '.' {
348+
continue
349+
}
350+
338351
isDir, err := g.isDir(fsys, dir, name, info)
339352
if err != nil {
340353
return err

globwalk_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type SkipTest struct {
1717
var skipTests = []SkipTest{
1818
{"a", "a", "a", 0, 0},
1919
{"a/", "a", "a", 1, 1},
20-
{"*", "b", "c", 12, 10},
20+
{"*", "b", "c", 15, 13},
2121
{"a/**", "a", "a", 0, 0},
2222
{"a/**", "a/abc", "a/b", 1, 1},
2323
{"a/**", "a/b/c", "a/b/c/d", 5, 5},

0 commit comments

Comments
 (0)