1// Copyright 2014 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package pathtools
16
17import (
18	"errors"
19	"os"
20	"path/filepath"
21	"strings"
22)
23
24var GlobMultipleRecursiveErr = errors.New("pattern contains multiple **")
25var GlobLastRecursiveErr = errors.New("pattern ** as last path element")
26
27// Glob returns the list of files that match the given pattern along with the
28// list of directories that were searched to construct the file list.
29// The supported glob patterns are equivalent to filepath.Glob, with an
30// extension that recursive glob (** matching zero or more complete path
31// entries) is supported.
32func Glob(pattern string) (matches, dirs []string, err error) {
33	return GlobWithExcludes(pattern, nil)
34}
35
36// GlobWithExcludes returns the list of files that match the given pattern but
37// do not match the given exclude patterns, along with the list of directories
38// that were searched to construct the file list.  The supported glob and
39// exclude patterns are equivalent to filepath.Glob, with an extension that
40// recursive glob (** matching zero or more complete path entries) is supported.
41func GlobWithExcludes(pattern string, excludes []string) (matches, dirs []string, err error) {
42	if filepath.Base(pattern) == "**" {
43		return nil, nil, GlobLastRecursiveErr
44	} else {
45		matches, dirs, err = glob(pattern, false)
46	}
47
48	if err != nil {
49		return nil, nil, err
50	}
51
52	matches, err = filterExcludes(matches, excludes)
53	if err != nil {
54		return nil, nil, err
55	}
56
57	return matches, dirs, nil
58}
59
60// glob is a recursive helper function to handle globbing each level of the pattern individually,
61// allowing searched directories to be tracked.  Also handles the recursive glob pattern, **.
62func glob(pattern string, hasRecursive bool) (matches, dirs []string, err error) {
63	if !isWild(pattern) {
64		// If there are no wilds in the pattern, check whether the file exists or not.
65		// Uses filepath.Glob instead of manually statting to get consistent results.
66		pattern = filepath.Clean(pattern)
67		matches, err = filepath.Glob(pattern)
68		if err != nil {
69			return matches, dirs, err
70		}
71
72		if len(matches) == 0 {
73			// Some part of the non-wild pattern didn't exist.  Add the last existing directory
74			// as a dependency.
75			var matchDirs []string
76			for len(matchDirs) == 0 {
77				pattern, _ = saneSplit(pattern)
78				matchDirs, err = filepath.Glob(pattern)
79				if err != nil {
80					return matches, dirs, err
81				}
82			}
83			dirs = append(dirs, matchDirs...)
84		}
85		return matches, dirs, err
86	}
87
88	dir, file := saneSplit(pattern)
89
90	if file == "**" {
91		if hasRecursive {
92			return matches, dirs, GlobMultipleRecursiveErr
93		}
94		hasRecursive = true
95	}
96
97	dirMatches, dirs, err := glob(dir, hasRecursive)
98	if err != nil {
99		return nil, nil, err
100	}
101
102	for _, m := range dirMatches {
103		if info, _ := os.Stat(m); info.IsDir() {
104			if file == "**" {
105				recurseDirs, err := walkAllDirs(m)
106				if err != nil {
107					return nil, nil, err
108				}
109				matches = append(matches, recurseDirs...)
110			} else {
111				dirs = append(dirs, m)
112				newMatches, err := filepath.Glob(filepath.Join(m, file))
113				if err != nil {
114					return nil, nil, err
115				}
116				matches = append(matches, newMatches...)
117			}
118		}
119	}
120
121	return matches, dirs, nil
122}
123
124// Faster version of dir, file := filepath.Dir(path), filepath.File(path) with no allocations
125// Similar to filepath.Split, but returns "." if dir is empty and trims trailing slash if dir is
126// not "/".  Returns ".", "" if path is "."
127func saneSplit(path string) (dir, file string) {
128	if path == "." {
129		return ".", ""
130	}
131	dir, file = filepath.Split(path)
132	switch dir {
133	case "":
134		dir = "."
135	case "/":
136		// Nothing
137	default:
138		dir = dir[:len(dir)-1]
139	}
140	return dir, file
141}
142
143func isWild(pattern string) bool {
144	return strings.ContainsAny(pattern, "*?[")
145}
146
147// Returns a list of all directories under dir
148func walkAllDirs(dir string) (dirs []string, err error) {
149	err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
150		if err != nil {
151			return err
152		}
153
154		if info.Mode().IsDir() {
155			dirs = append(dirs, path)
156		}
157		return nil
158	})
159
160	return dirs, err
161}
162
163// Filters the strings in matches based on the glob patterns in excludes.  Hierarchical (a/*) and
164// recursive (**) glob patterns are supported.
165func filterExcludes(matches []string, excludes []string) ([]string, error) {
166	if len(excludes) == 0 {
167		return matches, nil
168	}
169
170	var ret []string
171matchLoop:
172	for _, m := range matches {
173		for _, e := range excludes {
174			exclude, err := match(e, m)
175			if err != nil {
176				return nil, err
177			}
178			if exclude {
179				continue matchLoop
180			}
181		}
182		ret = append(ret, m)
183	}
184
185	return ret, nil
186}
187
188// match returns true if name matches pattern using the same rules as filepath.Match, but supporting
189// hierarchical patterns (a/*) and recursive globs (**).
190func match(pattern, name string) (bool, error) {
191	if filepath.Base(pattern) == "**" {
192		return false, GlobLastRecursiveErr
193	}
194
195	for {
196		var patternFile, nameFile string
197		pattern, patternFile = saneSplit(pattern)
198		name, nameFile = saneSplit(name)
199
200		if patternFile == "**" {
201			return matchPrefix(pattern, filepath.Join(name, nameFile))
202		}
203
204		if nameFile == "" && patternFile == "" {
205			return true, nil
206		} else if nameFile == "" || patternFile == "" {
207			return false, nil
208		}
209
210		match, err := filepath.Match(patternFile, nameFile)
211		if err != nil || !match {
212			return match, err
213		}
214	}
215}
216
217// matchPrefix returns true if the beginning of name matches pattern using the same rules as
218// filepath.Match, but supporting hierarchical patterns (a/*).  Recursive globs (**) are not
219// supported, they should have been handled in match().
220func matchPrefix(pattern, name string) (bool, error) {
221	if len(pattern) > 0 && pattern[0] == '/' {
222		if len(name) > 0 && name[0] == '/' {
223			pattern = pattern[1:]
224			name = name[1:]
225		} else {
226			return false, nil
227		}
228	}
229
230	for {
231		var patternElem, nameElem string
232		patternElem, pattern = saneSplitFirst(pattern)
233		nameElem, name = saneSplitFirst(name)
234
235		if patternElem == "." {
236			patternElem = ""
237		}
238		if nameElem == "." {
239			nameElem = ""
240		}
241
242		if patternElem == "**" {
243			return false, GlobMultipleRecursiveErr
244		}
245
246		if patternElem == "" {
247			return true, nil
248		} else if nameElem == "" {
249			return false, nil
250		}
251
252		match, err := filepath.Match(patternElem, nameElem)
253		if err != nil || !match {
254			return match, err
255		}
256	}
257}
258
259func saneSplitFirst(path string) (string, string) {
260	i := strings.IndexRune(path, filepath.Separator)
261	if i < 0 {
262		return path, ""
263	}
264	return path[:i], path[i+1:]
265}
266
267func GlobPatternList(patterns []string, prefix string) (globedList []string, depDirs []string, err error) {
268	var (
269		matches []string
270		deps    []string
271	)
272
273	globedList = make([]string, 0)
274	depDirs = make([]string, 0)
275
276	for _, pattern := range patterns {
277		if isWild(pattern) {
278			matches, deps, err = Glob(filepath.Join(prefix, pattern))
279			if err != nil {
280				return nil, nil, err
281			}
282			globedList = append(globedList, matches...)
283			depDirs = append(depDirs, deps...)
284		} else {
285			globedList = append(globedList, filepath.Join(prefix, pattern))
286		}
287	}
288	return globedList, depDirs, nil
289}
290