Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: file paths on windows conflict with the ast escape rune #12

Merged
merged 1 commit into from
Dec 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 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,16 @@ func QuoteMeta(pattern string) string {
return glob.QuoteMeta(pattern)
}

// toNixPath converts the path to the nix style path
// Windows style path separators are escape characters so cause issues with the compiled glob.
func toNixPath(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 +71,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 +87,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 +111,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 = toNixPath(path)
if !matcher.Match(path) {
return nil
}
Expand Down Expand Up @@ -149,6 +162,7 @@ func filesInDirectory(fs afero.Fs, dir string) ([]string, error) {
if info.IsDir() {
return nil
}
path = toNixPath(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
16 changes: 8 additions & 8 deletions prefix.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package fileglob

import (
"fmt"
"path/filepath"
"strings"

"github.com/gobwas/glob/syntax/ast"
Expand Down Expand Up @@ -59,13 +58,10 @@ 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))

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

// nolint:prealloc
var prefixPath []string
for _, part := range parts {
if part == "" {
continue
Expand All @@ -81,7 +77,11 @@ func staticPrefix(pattern string) (string, error) {
break
}

prefix = filepath.Join(prefix, staticPart)
prefixPath = append(prefixPath, staticPart)
}
prefix := strings.Join(prefixPath, stringSeparator)
if len(pattern) > 0 && rune(pattern[0]) == runeSeparator && !strings.HasPrefix(prefix, stringSeparator) {
prefix = stringSeparator + prefix
}

if prefix == "" {
Expand Down
4 changes: 4 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 @@ -22,6 +23,7 @@ func TestStaticPrefix(t *testing.T) {
{"./", "."},
{"fo\\*o/bar/b*z", "fo*o/bar"},
{"/\\{foo\\}/bar", "/{foo}/bar"},
{"C:/Path/To/Some/File", "C:/Path/To/Some/File"},
}

for _, testCase := range testCases {
Expand All @@ -32,6 +34,7 @@ func TestStaticPrefix(t *testing.T) {
}

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

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