From 13808e841eb95426f1d311bb2386d8561dda231a Mon Sep 17 00:00:00 2001 From: Elara Musayelyan Date: Sat, 21 May 2022 15:20:29 -0700 Subject: [PATCH] Add filesystem glob support --- glob.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++ glob_test.go | 83 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) diff --git a/glob.go b/glob.go index 232a449..30f4920 100644 --- a/glob.go +++ b/glob.go @@ -1,6 +1,10 @@ package pcre import ( + "io/fs" + "os" + "path/filepath" + "strings" "unsafe" "go.arsenm.dev/pcre/lib" @@ -66,3 +70,93 @@ func CompileGlob(glob string) (*Regexp, error) { // Compile converted glob and return results return Compile(pattern) } + +// Glob returns a list of matches for the given glob pattern. +// It returns nil if there was no match. If the glob contains +// "**", it will recurse through the directory, which may be +// extremely slow depending on which directory is being searched. +func Glob(glob string) ([]string, error) { + // If glob is empty, return nil + if glob == "" { + return nil, nil + } + + // If the glob is a file path, return the file + _, err := os.Lstat(glob) + if err == nil { + return []string{glob}, nil + } + + // If the glob has no glob characters, return nil + if !hasGlobChars(glob) { + return nil, nil + } + + // Split glob by filepath separator + paths := strings.Split(glob, string(filepath.Separator)) + + var splitDir []string + // For every path in split list + for _, path := range paths { + // If glob characters forund, stop + if hasGlobChars(path) { + break + } + // Add path to splitDir + splitDir = append(splitDir, path) + } + + // Join splitDir and add filepath separator. This is the directory that will be searched. + dir := filepath.Join(splitDir...) + + if filepath.IsAbs(glob) { + dir = string(filepath.Separator) + dir + } + + // If the directory is not accessible, return error + _, err = os.Lstat(dir) + if err != nil { + return nil, err + } + + // Compile glob pattern + r, err := CompileGlob(glob) + if err != nil { + return nil, err + } + defer r.Close() + + var matches []string + // If glob contains "**" (starstar), walk recursively. Otherwise, only search dir. + if strings.Contains(glob, "**") { + err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if r.MatchString(path) { + matches = append(matches, path) + } + return nil + }) + if err != nil { + return nil, err + } + } else { + files, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + for _, file := range files { + // Get full path of file + path := filepath.Join(dir, file.Name()) + if r.MatchString(path) { + matches = append(matches, path) + } + } + } + + return matches, nil +} + +// hasGlobChars checks if the string has any +// characters that are part of a glob. +func hasGlobChars(s string) bool { + return strings.ContainsAny(s, "*[]?") +} diff --git a/glob_test.go b/glob_test.go index 3d0f0b9..4b6ddda 100644 --- a/glob_test.go +++ b/glob_test.go @@ -1,6 +1,7 @@ package pcre_test import ( + "os" "testing" "go.arsenm.dev/pcre" @@ -36,3 +37,85 @@ func TestCompileGlob(t *testing.T) { t.Error("expected /home not to match") } } + +func TestGlob(t *testing.T) { + err := os.MkdirAll("pcretest/dir1", 0755) + if err != nil { + t.Fatal(err) + } + + err = os.MkdirAll("pcretest/dir2", 0755) + if err != nil { + t.Fatal(err) + } + + err = os.MkdirAll("pcretest/test1/dir4", 0755) + if err != nil { + t.Fatal(err) + } + + err = touch("pcretest/file1") + if err != nil { + t.Fatal(err) + } + + err = touch("pcretest/file2") + if err != nil { + t.Fatal(err) + } + + err = touch("pcretest/test1/dir4/text.txt") + if err != nil { + t.Fatal(err) + } + + matches, err := pcre.Glob("pcretest") + if err != nil { + t.Fatal(err) + } + if len(matches) != 1 || matches[0] != "pcretest" { + t.Errorf("expected [pcretest], got %v", matches) + } + + matches, err = pcre.Glob("pcretest/dir*") + if err != nil { + t.Fatal(err) + } + if len(matches) != 2 || + matches[0] != "pcretest/dir1" || + matches[1] != "pcretest/dir2" { + t.Errorf("expected [pcretest/dir1 pcretest/dir2], got %v", matches) + } + + matches, err = pcre.Glob("pcretest/file*") + if err != nil { + t.Fatal(err) + } + if len(matches) != 2 || + matches[0] != "pcretest/file1" || + matches[1] != "pcretest/file2" { + t.Errorf("expected [pcretest/file1 pcretest/file2], got %v", matches) + } + + matches, err = pcre.Glob("pcretest/**/*.txt") + if err != nil { + t.Fatal(err) + } + if len(matches) != 1 || + matches[0] != "pcretest/test1/dir4/text.txt" { + t.Errorf("expected [pcretest/test1/dir4/text.txt], got %v", matches) + } + + err = os.RemoveAll("pcretest") + if err != nil { + t.Fatal(err) + } +} + +func touch(path string) error { + fl, err := os.OpenFile(path, os.O_CREATE, 0644) + if err != nil { + return err + } + return fl.Close() +} \ No newline at end of file