Skip to content

Commit

Permalink
fix: file paths on windows conflict with the ast escape rune so we ne…
Browse files Browse the repository at this point in the history
…ed to keep paths in the linux style
  • Loading branch information
Dj Gilcrease committed Dec 8, 2020
1 parent b27e0ce commit 679b41a
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 7 deletions.
18 changes: 15 additions & 3 deletions glob.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import (
"github.com/spf13/afero"
)

const (
runeSeparator = '/'
stringSeparator = string(runeSeparator)
)

// FileSystem is meant to be used with WithFs.
type FileSystem afero.Fs

Expand Down Expand Up @@ -48,10 +53,14 @@ func QuoteMeta(pattern string) string {
return glob.QuoteMeta(pattern)
}

func CleanPath(path string) string {
return filepath.ToSlash(filepath.Clean(path))
}

// Glob returns all files that match the given pattern in the current directory.
func Glob(pattern string, opts ...OptFunc) ([]string, error) {
return doGlob(
strings.TrimPrefix(pattern, "./"),
strings.TrimPrefix(pattern, "."+stringSeparator),
compileOptions(opts),
)
}
Expand All @@ -60,7 +69,7 @@ func doGlob(pattern string, options *globOptions) ([]string, error) { // nolint:
var fs = options.fs
var matches []string

matcher, err := glob.Compile(pattern, filepath.Separator)
matcher, err := glob.Compile(pattern, runeSeparator)
if err != nil {
return matches, fmt.Errorf("compile glob pattern: %w", err)
}
Expand All @@ -76,7 +85,7 @@ func doGlob(pattern string, options *globOptions) ([]string, error) { // nolint:
// glob contains no dynamic matchers so prefix is the file name that
// the glob references directly. When the glob explicitly references
// a single non-existing file, return an error for the user to check.
return []string{}, fmt.Errorf("matching %q: %w", prefix, os.ErrNotExist)
return []string{}, fmt.Errorf(`matching "%s": %w`, prefix, os.ErrNotExist)
}

return []string{}, nil
Expand All @@ -100,6 +109,8 @@ func doGlob(pattern string, options *globOptions) ([]string, error) { // nolint:
return err
}

// The glob ast from github.com/gobwas/glob only works properly with linux paths
path = CleanPath(path)
if !matcher.Match(path) {
return nil
}
Expand Down Expand Up @@ -149,6 +160,7 @@ func filesInDirectory(fs afero.Fs, dir string) ([]string, error) {
if info.IsDir() {
return nil
}
path = CleanPath(path)
files = append(files, path)
return nil
})
Expand Down
25 changes: 25 additions & 0 deletions glob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (
)

func TestGlob(t *testing.T) { // nolint:funlen
t.Parallel()
t.Run("real", func(t *testing.T) {
t.Parallel()
matches, err := Glob("*_test.go")
require.NoError(t, err)
require.Equal(t, []string{
Expand All @@ -21,6 +23,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("simple", func(t *testing.T) {
t.Parallel()
matches, err := Glob("./a/*/*", WithFs(testFs(t, []string{
"./c/file1.txt",
"./a/nope/file1.txt",
Expand All @@ -39,6 +42,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("single file", func(t *testing.T) {
t.Parallel()
matches, err := Glob("a/b/*", WithFs(testFs(t, []string{
"./c/file1.txt",
"./a/nope/file1.txt",
Expand All @@ -49,6 +53,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("super asterisk", func(t *testing.T) {
t.Parallel()
matches, err := Glob("a/**/*", WithFs(testFs(t, []string{
"./a/nope.txt",
"./a/d/file1.txt",
Expand All @@ -64,6 +69,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("alternative matchers", func(t *testing.T) {
t.Parallel()
matches, err := Glob("a/{b,d}/file.txt", WithFs(testFs(t, []string{
"a/b/file.txt",
"a/c/file.txt",
Expand All @@ -77,6 +83,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("character list and range matchers", func(t *testing.T) {
t.Parallel()
matches, err := Glob("[!bc]/[a-z]/file[01].txt", WithFs(testFs(t, []string{
"a/b/file0.txt",
"a/c/file1.txt",
Expand All @@ -93,6 +100,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("nested matchers", func(t *testing.T) {
t.Parallel()
matches, err := Glob("{a,[0-9]b}.txt", WithFs(testFs(t, []string{
"a.txt",
"b.txt",
Expand All @@ -107,6 +115,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("single symbol wildcards", func(t *testing.T) {
t.Parallel()
matches, err := Glob("a?.txt", WithFs(testFs(t, []string{
"a.txt",
"a1.txt",
Expand All @@ -120,6 +129,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("direct match", func(t *testing.T) {
t.Parallel()
matches, err := Glob("a/b/c", WithFs(testFs(t, []string{
"./a/nope.txt",
"./a/b/c",
Expand All @@ -129,6 +139,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("direct match wildcard", func(t *testing.T) {
t.Parallel()
matches, err := Glob(QuoteMeta("a/b/c{a"), WithFs(testFs(t, []string{
"./a/nope.txt",
"a/b/c{a",
Expand All @@ -138,6 +149,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("direct no match", func(t *testing.T) {
t.Parallel()
matches, err := Glob("a/b/d", WithFs(testFs(t, []string{
"./a/nope.txt",
"./a/b/dc",
Expand All @@ -148,13 +160,15 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("escaped direct no match", func(t *testing.T) {
t.Parallel()
matches, err := Glob("a/\\{b\\}", WithFs(testFs(t, nil, nil)))
require.EqualError(t, err, "matching \"a/{b}\": file does not exist")
require.True(t, errors.Is(err, os.ErrNotExist))
require.Empty(t, matches)
})

t.Run("direct no match escaped wildcards", func(t *testing.T) {
t.Parallel()
matches, err := Glob(QuoteMeta("a/b/c{a"), WithFs(testFs(t, []string{
"./a/nope.txt",
"./a/b/dc",
Expand All @@ -164,6 +178,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("no matches", func(t *testing.T) {
t.Parallel()
matches, err := Glob("z/*", WithFs(testFs(t, []string{
"./a/nope.txt",
}, nil)))
Expand All @@ -172,12 +187,14 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("empty folder", func(t *testing.T) {
t.Parallel()
matches, err := Glob("a*", WithFs(testFs(t, nil, nil)))
require.NoError(t, err)
require.Empty(t, matches)
})

t.Run("escaped asterisk", func(t *testing.T) {
t.Parallel()
matches, err := Glob("a/\\*/b", WithFs(testFs(t, []string{
"a/a/b",
"a/*/b",
Expand All @@ -190,6 +207,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("escaped curly braces", func(t *testing.T) {
t.Parallel()
matches, err := Glob("\\{a,b\\}/c", WithFs(testFs(t, []string{
"a/c",
"b/c",
Expand All @@ -202,12 +220,14 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("invalid pattern", func(t *testing.T) {
t.Parallel()
matches, err := Glob("[*", WithFs(testFs(t, nil, nil)))
require.EqualError(t, err, "compile glob pattern: unexpected end of input")
require.Empty(t, matches)
})

t.Run("prefix is a file", func(t *testing.T) {
t.Parallel()
matches, err := Glob("ab/c/*", WithFs(testFs(t, []string{
"ab/c",
"ab/d",
Expand All @@ -218,6 +238,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("match files in directories", func(t *testing.T) {
t.Parallel()
matches, err := Glob("/a/{b,c}", WithFs(testFs(t, []string{
"/a/b/d",
"/a/b/e/f",
Expand All @@ -232,6 +253,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("match directories directly", func(t *testing.T) {
t.Parallel()
matches, err := Glob("/a/{b,c}", MatchDirectories(true), WithFs(testFs(t, []string{
"/a/b/d",
"/a/b/e/f",
Expand All @@ -245,6 +267,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("match empty directory", func(t *testing.T) {
t.Parallel()
matches, err := Glob("/a/{b,c}", MatchDirectories(true), WithFs(testFs(t, []string{
"/a/b",
}, []string{
Expand All @@ -258,6 +281,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
})

t.Run("pattern ending with star and subdir", func(t *testing.T) {
t.Parallel()
matches, err := Glob("a/*", WithFs(testFs(t, []string{
"./a/1.txt",
"./a/2.txt",
Expand All @@ -278,6 +302,7 @@ func TestGlob(t *testing.T) { // nolint:funlen
}

func TestQuoteMeta(t *testing.T) {
t.Parallel()
matches, err := Glob(QuoteMeta("{a,b}/c"), WithFs(testFs(t, []string{
"a/c",
"b/c",
Expand Down
9 changes: 5 additions & 4 deletions prefix.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ func staticText(node *ast.Node) (text string, ok bool) {
// staticPrefix returns the file path inside the pattern up
// to the first path element that contains a wildcard.
func staticPrefix(pattern string) (string, error) {
parts := strings.Split(pattern, string(filepath.Separator))
parts := strings.Split(pattern, stringSeparator)

prefix := ""
if len(pattern) > 0 && rune(pattern[0]) == filepath.Separator {
prefix = string(filepath.Separator)
if len(pattern) > 0 && rune(pattern[0]) == runeSeparator {
prefix = stringSeparator
}

for _, part := range parts {
Expand All @@ -81,7 +81,8 @@ func staticPrefix(pattern string) (string, error) {
break
}

prefix = filepath.Join(prefix, staticPart)
// The glob ast from github.com/gobwas/glob only works properly with linux paths
prefix = filepath.ToSlash(filepath.Join(prefix, staticPart))
}

if prefix == "" {
Expand Down
3 changes: 3 additions & 0 deletions prefix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
)

func TestStaticPrefix(t *testing.T) {
t.Parallel()
var testCases = []struct {
pattern string
prefix string
Expand All @@ -32,6 +33,7 @@ func TestStaticPrefix(t *testing.T) {
}

func TestContainsMatchers(t *testing.T) {
t.Parallel()
var testCases = []struct {
pattern string
containsMatchers bool
Expand All @@ -57,6 +59,7 @@ func TestContainsMatchers(t *testing.T) {
}

func TestValidPattern(t *testing.T) {
t.Parallel()
var testCases = []struct {
pattern string
valid bool
Expand Down

0 comments on commit 679b41a

Please sign in to comment.