github.com/mattn/go-zglob@v0.0.5-0.20230108120541-9c2404f90757/zglob.go (about)

     1  package zglob
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  	"path"
     8  	"path/filepath"
     9  	"regexp"
    10  	"runtime"
    11  	"strings"
    12  	"sync"
    13  
    14  	"github.com/mattn/go-zglob/fastwalk"
    15  )
    16  
    17  var (
    18  	envre = regexp.MustCompile(`^(\$[a-zA-Z][a-zA-Z0-9_]+|\$\([a-zA-Z][a-zA-Z0-9_]+\))$`)
    19  	mu    sync.Mutex
    20  )
    21  
    22  type zenv struct {
    23  	dirmask  string
    24  	fre      *regexp.Regexp
    25  	braceDir bool
    26  	pattern  string
    27  	root     string
    28  }
    29  
    30  func toSlash(path string) string {
    31  	if filepath.Separator == '/' {
    32  		return path
    33  	}
    34  	var buf bytes.Buffer
    35  	cc := []rune(path)
    36  	for i := 0; i < len(cc); i++ {
    37  		if i < len(cc)-2 && cc[i] == '\\' && (cc[i+1] == '{' || cc[i+1] == '}') {
    38  			buf.WriteRune(cc[i])
    39  			buf.WriteRune(cc[i+1])
    40  			i++
    41  		} else if cc[i] == '\\' {
    42  			buf.WriteRune('/')
    43  		} else {
    44  			buf.WriteRune(cc[i])
    45  		}
    46  	}
    47  	return buf.String()
    48  }
    49  
    50  func New(pattern string) (*zenv, error) {
    51  	globmask := ""
    52  	root := ""
    53  	for n, i := range strings.Split(toSlash(pattern), "/") {
    54  		if root == "" && (strings.Index(i, "*") != -1 || strings.Index(i, "{") != -1) {
    55  			if globmask == "" {
    56  				root = "."
    57  			} else {
    58  				root = toSlash(globmask)
    59  			}
    60  		}
    61  		if n == 0 && i == "~" {
    62  			if runtime.GOOS == "windows" {
    63  				i = os.Getenv("USERPROFILE")
    64  			} else {
    65  				i = os.Getenv("HOME")
    66  			}
    67  		}
    68  		if envre.MatchString(i) {
    69  			i = strings.Trim(strings.Trim(os.Getenv(i[1:]), "()"), `"`)
    70  		}
    71  
    72  		globmask = path.Join(globmask, i)
    73  		if n == 0 {
    74  			if runtime.GOOS == "windows" && filepath.VolumeName(i) != "" {
    75  				globmask = i + "/"
    76  			} else if len(globmask) == 0 {
    77  				globmask = "/"
    78  			}
    79  		}
    80  	}
    81  	if root == "" {
    82  		return &zenv{
    83  			dirmask: "",
    84  			fre:     nil,
    85  			pattern: pattern,
    86  			root:    "",
    87  		}, nil
    88  	}
    89  	if globmask == "" {
    90  		globmask = "."
    91  	}
    92  	globmask = toSlash(path.Clean(globmask))
    93  
    94  	cc := []rune(globmask)
    95  	dirmask := ""
    96  	filemask := ""
    97  	staticDir := true
    98  	for i := 0; i < len(cc); i++ {
    99  		if i < len(cc)-2 && cc[i] == '\\' {
   100  			i++
   101  			filemask += fmt.Sprintf("[\\x%02X]", cc[i])
   102  			if staticDir {
   103  				dirmask += string(cc[i])
   104  			}
   105  		} else if cc[i] == '*' {
   106  			staticDir = false
   107  			if i < len(cc)-2 && cc[i+1] == '*' && cc[i+2] == '/' {
   108  				filemask += "(.*/)?"
   109  				i += 2
   110  			} else {
   111  				filemask += "[^/]*"
   112  			}
   113  		} else {
   114  			if cc[i] == '{' {
   115  				staticDir = false
   116  				pattern := ""
   117  				for j := i + 1; j < len(cc); j++ {
   118  					if cc[j] == ',' {
   119  						pattern += "|"
   120  					} else if cc[j] == '}' {
   121  						i = j
   122  						break
   123  					} else {
   124  						c := cc[j]
   125  						if c == '/' {
   126  							pattern += string(c)
   127  						} else if ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || 255 < c {
   128  							pattern += string(c)
   129  						} else {
   130  							pattern += fmt.Sprintf("[\\x%02X]", c)
   131  						}
   132  					}
   133  				}
   134  				if pattern != "" {
   135  					filemask += "(" + pattern + ")"
   136  					continue
   137  				}
   138  			} else if i < len(cc)-1 && cc[i] == '!' && cc[i+1] == '(' {
   139  				i++
   140  				pattern := ""
   141  				for j := i + 1; j < len(cc); j++ {
   142  					if cc[j] == ')' {
   143  						i = j
   144  						break
   145  					} else {
   146  						c := cc[j]
   147  						pattern += fmt.Sprintf("[^\\x%02X/]*", c)
   148  					}
   149  				}
   150  				if pattern != "" {
   151  					if dirmask == "" {
   152  						dirmask = filemask
   153  						root = filemask
   154  					}
   155  					filemask += pattern
   156  					continue
   157  				}
   158  			}
   159  			c := cc[i]
   160  			if c == '/' || ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || 255 < c {
   161  				filemask += string(c)
   162  			} else {
   163  				filemask += fmt.Sprintf("[\\x%02X]", c)
   164  			}
   165  			if staticDir {
   166  				dirmask += string(c)
   167  			}
   168  		}
   169  	}
   170  	if len(filemask) > 0 && filemask[len(filemask)-1] == '/' {
   171  		if root == "" {
   172  			root = filemask
   173  		}
   174  		filemask += "[^/]*"
   175  	}
   176  	if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
   177  		filemask = "(?i:" + filemask + ")"
   178  	}
   179  	return &zenv{
   180  		dirmask: path.Dir(dirmask) + "/",
   181  		fre:     regexp.MustCompile("^" + filemask + "$"),
   182  		pattern: pattern,
   183  		root:    filepath.Clean(root),
   184  	}, nil
   185  }
   186  
   187  func Glob(pattern string) ([]string, error) {
   188  	return glob(pattern, false)
   189  }
   190  
   191  func GlobFollowSymlinks(pattern string) ([]string, error) {
   192  	return glob(pattern, true)
   193  }
   194  
   195  func glob(pattern string, followSymlinks bool) ([]string, error) {
   196  	zenv, err := New(pattern)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	if zenv.root == "" {
   201  		_, err := os.Stat(pattern)
   202  		if err != nil {
   203  			return nil, os.ErrNotExist
   204  		}
   205  		return []string{pattern}, nil
   206  	}
   207  	relative := !filepath.IsAbs(pattern)
   208  	matches := []string{}
   209  
   210  	err = fastwalk.FastWalk(zenv.root, func(path string, info os.FileMode) error {
   211  		if zenv.root == "." && len(zenv.root) < len(path) {
   212  			path = path[len(zenv.root)+1:]
   213  		}
   214  		path = filepath.ToSlash(path)
   215  
   216  		if followSymlinks && info == os.ModeSymlink {
   217  			followedPath, err := filepath.EvalSymlinks(path)
   218  			if err == nil {
   219  				fi, err := os.Lstat(followedPath)
   220  				if err == nil && fi.IsDir() {
   221  					return fastwalk.TraverseLink
   222  				}
   223  			}
   224  		}
   225  
   226  		if info.IsDir() {
   227  			if path == "." || len(path) <= len(zenv.root) {
   228  				return nil
   229  			}
   230  			if zenv.fre.MatchString(path) {
   231  				mu.Lock()
   232  				matches = append(matches, path)
   233  				mu.Unlock()
   234  				return nil
   235  			}
   236  			if len(path) < len(zenv.dirmask) && !strings.HasPrefix(zenv.dirmask, path+"/") {
   237  				return filepath.SkipDir
   238  			}
   239  		}
   240  
   241  		if zenv.fre.MatchString(path) {
   242  			if relative && filepath.IsAbs(path) {
   243  				path = path[len(zenv.root)+1:]
   244  			}
   245  			mu.Lock()
   246  			matches = append(matches, path)
   247  			mu.Unlock()
   248  		}
   249  		return nil
   250  	})
   251  
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	return matches, nil
   257  }
   258  
   259  func Match(pattern, name string) (matched bool, err error) {
   260  	zenv, err := New(pattern)
   261  	if err != nil {
   262  		return false, err
   263  	}
   264  	return zenv.Match(name), nil
   265  }
   266  
   267  func (z *zenv) Match(name string) bool {
   268  	if z.root == "" {
   269  		return z.pattern == name
   270  	}
   271  
   272  	name = filepath.ToSlash(name)
   273  
   274  	if name == "." || len(name) <= len(z.root) {
   275  		return false
   276  	}
   277  
   278  	if z.fre.MatchString(name) {
   279  		return true
   280  	}
   281  	return false
   282  }