Skip to content

Commit

Permalink
Allow case-insensitive filename globbing
Browse files Browse the repository at this point in the history
- Add a FoldCase flag to the expand.Config struct, which is used to
  configure how expand.Fields behaves.
- Similarly, add a FoldCase Mode to pattern.Regexp. Setting the flag
  adds (?i) to the front of the generated regular expression.
- Use the former to set the latter when calling expand.Fields.

Rationale: I use expand.Fields to do filename expansion, for which I'd
like to do case-insensitive matching. E.g. I'd like "c*" to match both
"cmd" and "CHANGELOG.md"
  • Loading branch information
theclapp committed Apr 16, 2024
1 parent a89b0be commit f54bc69
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 2 deletions.
9 changes: 8 additions & 1 deletion expand/expand.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ type Config struct {
// as errors.
NoUnset bool

// FoldCase causes case-insensitive pattern matching in file-name globbing.
FoldCase bool

bufferAlloc bytes.Buffer // TODO: use strings.Builder
fieldAlloc [4]fieldPart
fieldsAlloc [4][]fieldPart
Expand Down Expand Up @@ -946,7 +949,11 @@ func (cfg *Config) glob(base, pat string) ([]string, error) {
}
continue
}
expr, err := pattern.Regexp(part, pattern.Filenames|pattern.EntireString)
mode := pattern.Filenames | pattern.EntireString
if cfg.FoldCase {
mode |= pattern.FoldCase
}
expr, err := pattern.Regexp(part, mode)
if err != nil {
return nil, err
}
Expand Down
50 changes: 50 additions & 0 deletions expand/expand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package expand

import (
"io/fs"
"os"
"reflect"
"strings"
Expand Down Expand Up @@ -90,3 +91,52 @@ func TestFieldsIdempotency(t *testing.T) {
}
}
}

func Test_glob(t *testing.T) {
cfg := &Config{
ReadDir2: func(string) ([]fs.DirEntry, error) {
return []fs.DirEntry{
&mockFileInfo{name: "a"},
&mockFileInfo{name: "ab"},
&mockFileInfo{name: "A"},
&mockFileInfo{name: "AB"},
}, nil
},
}

tests := []struct {
foldCase bool
pat string
want []string
}{
{false, "a*", []string{"a", "ab"}},
{false, "A*", []string{"A", "AB"}},
{false, "*b", []string{"ab"}},
{false, "b*", nil},
{true, "a*", []string{"a", "ab", "A", "AB"}},
{true, "A*", []string{"a", "ab", "A", "AB"}},
{true, "*b", []string{"ab", "AB"}},
{true, "b*", nil},
}
for _, tc := range tests {
cfg.FoldCase = tc.foldCase
got, err := cfg.glob("/", tc.pat)
if err != nil {
t.Fatalf("did not want error, got %v", err)
}
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("wanted %q, got %q", tc.want, got)
}
}
}

type mockFileInfo struct {
name string
typ fs.FileMode
fs.DirEntry // Stub out everything but Name() & Type()
}

var _ fs.DirEntry = (*mockFileInfo)(nil)

func (fi *mockFileInfo) Name() string { return fi.name }
func (fi *mockFileInfo) Type() fs.FileMode { return fi.typ }
6 changes: 5 additions & 1 deletion pattern/pattern.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
Filenames // "*" and "?" don't match slashes; only "**" does
Braces // support "{a,b}" and "{1..4}"
EntireString // match the entire string using ^$ delimiters
FoldCase // Do case-insensitive match (that is, use (?i) in the regexp)
)

var numRange = regexp.MustCompile(`^([+-]?\d+)\.\.([+-]?\d+)}`)
Expand Down Expand Up @@ -68,6 +69,9 @@ noopLoop:
// Enable matching `\n` with the `.` metacharacter as globs match `\n`
buf.WriteString("(?s)")
dotMeta := false
if mode&FoldCase != 0 {
buf.WriteString("(?i)")
}
if mode&EntireString != 0 {
buf.WriteString("^")
}
Expand Down Expand Up @@ -242,7 +246,7 @@ writeLoop:
if mode&EntireString != 0 {
buf.WriteString("$")
}
// No `.` metacharacters were used, so don't return the flag.
// No `.` metacharacters were used, so don't return the (?s) flag.
if !dotMeta {
return string(buf.Bytes()[4:]), nil
}
Expand Down

0 comments on commit f54bc69

Please sign in to comment.