github.com/ladydascalie/elvish@v0.0.0-20170703214355-2964dd3ece7f/glob/glob.go (about)

     1  // Package glob implements globbing for elvish.
     2  package glob
     3  
     4  import (
     5  	"io/ioutil"
     6  	"os"
     7  	"unicode/utf8"
     8  )
     9  
    10  // Glob returns a list of file names satisfying the given pattern.
    11  func Glob(p string, cb func(string) bool) bool {
    12  	return Parse(p).Glob(cb)
    13  }
    14  
    15  // Glob returns a list of file names satisfying the Pattern.
    16  func (p Pattern) Glob(cb func(string) bool) bool {
    17  	segs := p.Segments
    18  	dir := ""
    19  	if len(segs) > 0 && IsSlash(segs[0]) {
    20  		segs = segs[1:]
    21  		dir = "/"
    22  	}
    23  
    24  	if p.DirOverride != "" {
    25  		dir = p.DirOverride
    26  	}
    27  
    28  	return glob(segs, dir, cb)
    29  }
    30  
    31  func glob(segs []Segment, dir string, cb func(string) bool) bool {
    32  	// Consume the non-wildcard prefix. This is required so that "." and "..",
    33  	// which doesn't appear in the result of ReadDir, can appear as standalone
    34  	// path components in the pattern.
    35  	for len(segs) > 0 && IsLiteral(segs[0]) {
    36  		seg0 := segs[0].(Literal).Data
    37  		var path string
    38  		switch dir {
    39  		case "":
    40  			path = seg0
    41  		case "/":
    42  			path = "/" + seg0
    43  		default:
    44  			path = dir + "/" + seg0
    45  		}
    46  		if len(segs) == 1 {
    47  			// A lone literal. Generate it if the named file exists, and return.
    48  			if _, err := os.Stat(path); err == nil {
    49  				return cb(path)
    50  			}
    51  			return true
    52  		} else if IsSlash(segs[1]) {
    53  			// A lone literal followed by a slash. Change the directory if it
    54  			// exists, otherwise return.
    55  			if info, err := os.Stat(path); err == nil && info.IsDir() {
    56  				dir = path
    57  			} else {
    58  				return true
    59  			}
    60  			segs = segs[2:]
    61  		} else {
    62  			break
    63  		}
    64  	}
    65  
    66  	// Empty segment, resulting from a trailing slash. Generate the starting
    67  	// directory.
    68  	if len(segs) == 0 {
    69  		return cb(dir + "/")
    70  	}
    71  
    72  	var prefix string
    73  	if dir == "" {
    74  		prefix = ""
    75  		dir = "."
    76  	} else if dir == "/" {
    77  		prefix = "/"
    78  	} else {
    79  		// dir never has a trailing slash unless it is /.
    80  		prefix = dir + "/"
    81  	}
    82  
    83  	i := -1
    84  	nexti := func() {
    85  		for i++; i < len(segs); i++ {
    86  			if IsSlash(segs[i]) || IsWild1(segs[i], StarStar) {
    87  				break
    88  			}
    89  		}
    90  	}
    91  	nexti()
    92  
    93  	infos, err := ioutil.ReadDir(dir)
    94  	if err != nil {
    95  		// XXX Silently drop the error
    96  		return true
    97  	}
    98  
    99  	// Enumerate the position of the first slash.
   100  	for i < len(segs) {
   101  		slash := IsSlash(segs[i])
   102  		var first, rest []Segment
   103  		if slash {
   104  			// segs = x/y. Match dir with x, recurse on y.
   105  			first, rest = segs[:i], segs[i+1:]
   106  		} else {
   107  			// segs = x**y. Match dir with x*, recurse on **y.
   108  			first, rest = segs[:i+1], segs[i:]
   109  		}
   110  
   111  		for _, info := range infos {
   112  			name := info.Name()
   113  			if match(first, name) && info.IsDir() {
   114  				if !glob(rest, prefix+name, cb) {
   115  					return false
   116  				}
   117  			}
   118  		}
   119  
   120  		if slash {
   121  			// First slash cannot appear later than a slash in the pattern.
   122  			return true
   123  		}
   124  		nexti()
   125  	}
   126  
   127  	// If we reach here, it is possible to have no slashes at all.
   128  	for _, info := range infos {
   129  		name := info.Name()
   130  		if match(segs, name) {
   131  			if !cb(prefix + name) {
   132  				return false
   133  			}
   134  		}
   135  	}
   136  	return true
   137  }
   138  
   139  // match matches a name against segments. It treats StarStar segments as they
   140  // are Star segments. The segments may not contain Slash'es.
   141  func match(segs []Segment, name string) bool {
   142  	if len(segs) == 0 {
   143  		return name == ""
   144  	}
   145  	// If the name start with "." and the first segment is a Wild, only match
   146  	// when MatchHidden is true.
   147  	if len(name) > 0 && name[0] == '.' && IsWild(segs[0]) {
   148  		seg := segs[0].(Wild)
   149  		if !seg.MatchHidden {
   150  			return false
   151  		}
   152  	}
   153  segs:
   154  	for len(segs) > 0 {
   155  		// Find a chunk. A chunk is a run of Literal and Question, with an
   156  		// optional leading Star.
   157  		var i int
   158  		for i = 1; i < len(segs); i++ {
   159  			if IsWild2(segs[i], Star, StarStar) {
   160  				break
   161  			}
   162  		}
   163  
   164  		chunk := segs[:i]
   165  		startsWithStar := IsWild2(chunk[0], Star, StarStar)
   166  		var startingStar Wild
   167  		if startsWithStar {
   168  			startingStar = chunk[0].(Wild)
   169  			chunk = chunk[1:]
   170  		}
   171  		segs = segs[i:]
   172  
   173  		// NOTE A quick path when len(segs) == 0 can be implemented: match
   174  		// backwards.
   175  
   176  		// Match at the current position. If this is the last chunk, we need to
   177  		// make sure name is exhausted by the matching.
   178  		ok, rest := matchChunk(chunk, name)
   179  		if ok && (rest == "" || len(segs) > 0) {
   180  			name = rest
   181  			continue
   182  		}
   183  
   184  		if startsWithStar {
   185  			// NOTE An optimization is to make the upper bound not len(names),
   186  			// but rather len(names) - LB(# bytes segs can match)
   187  			for i, r := range name {
   188  				j := i + len(string(r))
   189  				// Match name[:j] with the starting *, and the rest with chunk.
   190  				if !startingStar.Match(r) {
   191  					break
   192  				}
   193  				ok, rest := matchChunk(chunk, name[j:])
   194  				if ok && (rest == "" || len(segs) > 0) {
   195  					name = rest
   196  					continue segs
   197  				}
   198  			}
   199  		}
   200  		return false
   201  	}
   202  	return name == ""
   203  }
   204  
   205  // matchChunk returns whether a chunk matches a prefix of name. If succeeded, it
   206  // also returns the remaining part of name.
   207  func matchChunk(chunk []Segment, name string) (bool, string) {
   208  	for _, seg := range chunk {
   209  		if name == "" {
   210  			return false, ""
   211  		}
   212  		switch seg := seg.(type) {
   213  		case Literal:
   214  			n := len(seg.Data)
   215  			if len(name) < n || name[:n] != seg.Data {
   216  				return false, ""
   217  			}
   218  			name = name[n:]
   219  		case Wild:
   220  			if seg.Type == Question {
   221  				r, n := utf8.DecodeRuneInString(name)
   222  				if !seg.Match(r) {
   223  					return false, ""
   224  				}
   225  				name = name[n:]
   226  			} else {
   227  				panic("chunk has non-question wild segment")
   228  			}
   229  		default:
   230  			panic("chunk has non-literal non-wild segment")
   231  		}
   232  	}
   233  	return true, name
   234  }