Skip to content
This repository has been archived by the owner on Dec 18, 2020. It is now read-only.

Commit

Permalink
improve globbing (fixes #91) (#95)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexanderwilling authored Mar 10, 2017
1 parent e05c793 commit 0c19a24
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 23 deletions.
29 changes: 27 additions & 2 deletions internal/paths/glob.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,37 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
)

var dirGlobOperator = string(filepath.Separator) + "**" + string(filepath.Separator)
var dirGlobOperator = "**"

// dirGlobOperatorUseValid returns false if '**' occurs, but '/**/' doesn't and pattern does not start with '**/'.
func dirGlobOperatorUseValid(pattern string) bool {
containsOperator := strings.Contains(pattern, dirGlobOperator)
operatorIsOwnPathSegment := strings.Contains(pattern, string(filepath.Separator)+dirGlobOperator+string(filepath.Separator))
startsWithOperator := strings.HasPrefix(pattern, dirGlobOperator+string(filepath.Separator))

return !containsOperator || (operatorIsOwnPathSegment || startsWithOperator)
}

// Glob supports * and ** globbing according to https://phraseapp.com/docs/developers/cli/configuration/#globbing
func Glob(pattern string) (matches []string, err error) {
pattern = filepath.Clean(pattern)
pattern = escape(pattern)

if strings.Count(pattern, dirGlobOperator) > 1 {
return nil, fmt.Errorf("invalid pattern '%s': the ** globbing operator may only be used once in a pattern", pattern)
}

if !dirGlobOperatorUseValid(pattern) {
return nil, fmt.Errorf("invalid pattern '%s': the ** globbing operator may only be used as path segment on its own, i.e. …/**/… or **/…", pattern)
}

if strings.Contains(pattern, dirGlobOperator) {
parts := strings.Split(pattern, dirGlobOperator)
basePattern, endPattern := parts[0], parts[1]
basePattern, endPattern := filepath.Clean(parts[0]), filepath.Clean(parts[1])

baseCandidates, err := filepath.Glob(basePattern)
if err != nil {
Expand Down Expand Up @@ -53,6 +68,16 @@ func Glob(pattern string) (matches []string, err error) {
return matches, nil
}

// escape escapes characters which filepath.Glob would otherwise handle in a special way (except on Windows...)
func escape(s string) string {
if runtime.GOOS == "windows" {
return s
}

s = strings.Replace(s, "?", "\\?", -1)
return strings.Replace(s, "[", "\\[", -1)
}

func filter(candidates []string, f func(os.FileInfo) bool) []string {
matches := []string{}
for _, candidate := range candidates {
Expand Down
95 changes: 74 additions & 21 deletions internal/paths/glob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@ import (
)

func TestGlob(t *testing.T) {
base, err := ioutil.TempDir("", "test-glob_")
defer os.RemoveAll(base)
if err != nil {
t.Error(err)
}

directories := []string{
"foo/bar/baz/asd",
"foo/bar/xyz/asd",
Expand All @@ -28,21 +22,6 @@ func TestGlob(t *testing.T) {
"nanana",
}

for _, dir := range directories {
err := os.MkdirAll(filepath.Join(base, dir), 0755)
defer os.RemoveAll(filepath.Join(base, dir))
if err != nil {
t.Error(err)
}

for _, file := range files {
_, err := os.Create(filepath.Join(base, dir, file))
if err != nil {
t.Error(err)
}
}
}

tests := map[string][]string{
"**/*.mp3": {},
"foo/*/baz/**/asd/*.yml": {"foo/bar/baz/asd/en.yml", "foo/bar/baz/xyz/asd/en.yml"},
Expand All @@ -66,6 +45,80 @@ func TestGlob(t *testing.T) {
},
}

testGlob(directories, files, tests, t)
}

func TestGlob_specialCharacters(t *testing.T) {
directories := []string{
"locales",
"foo?bar",
"bar[a-z]",
"bla/*",
}

files := []string{
"en.yml",
"?",
"_[^x]_.yml",
}

tests := map[string][]string{
"**/*.yml": {
"locales/en.yml",
"locales/_[^x]_.yml",
"foo?bar/en.yml",
"foo?bar/_[^x]_.yml",
"bar[a-z]/en.yml",
"bar[a-z]/_[^x]_.yml",
"bla/*/en.yml",
"bla/*/_[^x]_.yml",
},
"foo?bar/*": {
"foo?bar/en.yml",
"foo?bar/?",
"foo?bar/_[^x]_.yml",
},
"bar[a*/_[^x*]_.yml": {
"bar[a-z]/_[^x]_.yml",
},
"bla/\\*/*": {
"bla/*/en.yml",
"bla/*/?",
"bla/*/_[^x]_.yml",
},
"**/?": {
"locales/?",
"foo?bar/?",
"bar[a-z]/?",
"bla/*/?",
},
}

testGlob(directories, files, tests, t)
}

func testGlob(directories, files []string, tests map[string][]string, t *testing.T) {
base, err := ioutil.TempDir("", "test-glob_")
defer os.RemoveAll(base)
if err != nil {
t.Error(err)
}

for _, dir := range directories {
err := os.MkdirAll(filepath.Join(base, dir), 0755)
defer os.RemoveAll(filepath.Join(base, dir))
if err != nil {
t.Error(err)
}

for _, file := range files {
_, err := os.Create(filepath.Join(base, dir, file))
if err != nil {
t.Error(err)
}
}
}

for pattern, expected := range tests {
matches, err := Glob(filepath.Join(base, pattern))
if err != nil {
Expand Down

0 comments on commit 0c19a24

Please sign in to comment.