-
-
Notifications
You must be signed in to change notification settings - Fork 8
/
glob.go
242 lines (203 loc) · 6.3 KB
/
glob.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
package fileglob
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/gobwas/glob"
)
const (
separatorRune = '/'
separatorString = string(separatorRune)
)
type globOptions struct {
fs fs.FS
// if matchDirectories directly is set to true a matching directory will
// be treated just like a matching file. If set to false, a matching directory
// will auto-match all files inside instead of the directory itself.
matchDirectoriesDirectly bool
prefix string
pattern string
}
// OptFunc is a function that allow to customize Glob.
type OptFunc func(opts *globOptions)
// WithFs allows to provide another fs.FS implementation to Glob.
func WithFs(f fs.FS) OptFunc {
return func(opts *globOptions) {
opts.fs = f
}
}
// MaybeRootFS setups fileglob to walk from the root directory (/) or
// volume (on windows) if the given pattern is an absolute path.
//
// Result will also be prepended with the root path or volume.
func MaybeRootFS(opts *globOptions) {
if !filepath.IsAbs(opts.pattern) {
return
}
prefix := ""
if strings.HasPrefix(opts.pattern, separatorString) {
prefix = separatorString
}
if vol := filepath.VolumeName(opts.pattern); vol != "" {
prefix = vol + "/"
}
if prefix != "" {
opts.prefix = prefix
opts.fs = os.DirFS(prefix)
}
}
// WriteOptions write the current options to the given writer.
func WriteOptions(w io.Writer) OptFunc {
return func(opts *globOptions) {
_, _ = fmt.Fprintf(w, "%+v", opts)
}
}
// MatchDirectoryIncludesContents makes a match on a directory match all
// files inside it as well.
//
// This is the default behavior.
//
// Also check MatchDirectoryAsFile.
func MatchDirectoryIncludesContents(opts *globOptions) {
opts.matchDirectoriesDirectly = false
}
// MatchDirectoryAsFile makes a match on a directory match its name only.
//
// Also check MatchDirectoryIncludesContents.
func MatchDirectoryAsFile(opts *globOptions) {
opts.matchDirectoriesDirectly = true
}
// QuoteMeta quotes all glob pattern meta characters inside the argument text.
// For example, QuoteMeta for a pattern `{foo*}` sets the pattern to `\{foo\*\}`.
func QuoteMeta(opts *globOptions) {
opts.pattern = glob.QuoteMeta(opts.pattern)
}
// toNixPath converts the path to the nix style path
// Windows style path separators are escape characters so cause issues with the compiled glob.
func toNixPath(s string) string {
return filepath.ToSlash(filepath.Clean(s))
}
// Glob returns all files that match the given pattern in the current directory.
// If the given pattern indicates an absolute path, it will glob from `/`.
// If the given pattern starts with `../`, it will resolve to its absolute path and glob from `/`.
func Glob(pattern string, opts ...OptFunc) ([]string, error) { // nolint:funlen,cyclop
var matches []string
if strings.HasPrefix(pattern, "../") {
p, err := filepath.Abs(pattern)
if err != nil {
return matches, fmt.Errorf("failed to resolve pattern: %s: %w", pattern, err)
}
pattern = filepath.ToSlash(p)
}
options := compileOptions(opts, pattern)
pattern = strings.TrimSuffix(strings.TrimPrefix(options.pattern, options.prefix), separatorString)
matcher, err := glob.Compile(pattern, separatorRune)
if err != nil {
return matches, fmt.Errorf("compile glob pattern: %w", err)
}
prefix, err := staticPrefix(pattern)
if err != nil {
return nil, fmt.Errorf("cannot determine static prefix: %w", err)
}
// Check if the file is valid symlink without following it
// It works only for valid absolut or relative file paths, in other words, will fail for WithFs() option
if patternInfo, err := os.Lstat(pattern); err == nil { // nolint:govet
if patternInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
return cleanFilepaths([]string{pattern}, options.prefix), nil
}
}
prefixInfo, err := fs.Stat(options.fs, prefix)
if errors.Is(err, fs.ErrNotExist) {
if !ContainsMatchers(pattern) {
// glob contains no dynamic matchers so prefix is the file name that
// the glob references directly. When the glob explicitly references
// a single non-existing file, return an error for the user to check.
return []string{}, fmt.Errorf(`matching "%s%s": %w`, options.prefix, prefix, fs.ErrNotExist)
}
return []string{}, nil
}
if err != nil {
return nil, fmt.Errorf("stat static prefix %s%s: %w", options.prefix, prefix, err)
}
if !prefixInfo.IsDir() {
// if the prefix is a file, it either has to be
// the only match, or nothing matches at all
if matcher.Match(prefix) {
return cleanFilepaths([]string{prefix}, options.prefix), nil
}
return []string{}, nil
}
if err := fs.WalkDir(options.fs, prefix, func(path string, info fs.DirEntry, err error) error {
if err != nil {
return err
}
// The glob ast from github.com/gobwas/glob only works properly with linux paths
path = toNixPath(path)
if !matcher.Match(path) {
return nil
}
if info.IsDir() {
if options.matchDirectoriesDirectly {
matches = append(matches, path)
return nil
}
// a direct match on a directory implies that all files inside
// match if options.matchFolders is false
filesInDir, err := filesInDirectory(options, path)
if err != nil {
return err
}
matches = append(matches, filesInDir...)
return fs.SkipDir
}
matches = append(matches, path)
return nil
}); err != nil {
return nil, fmt.Errorf("glob failed: %w", err)
}
return cleanFilepaths(matches, options.prefix), nil
}
func compileOptions(optFuncs []OptFunc, pattern string) *globOptions {
opts := &globOptions{
fs: os.DirFS("."),
prefix: "./",
pattern: pattern,
}
for _, apply := range optFuncs {
apply(opts)
}
return opts
}
func filesInDirectory(options *globOptions, dir string) ([]string, error) {
var files []string
err := fs.WalkDir(options.fs, dir, func(path string, info fs.DirEntry, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
path = toNixPath(path)
files = append(files, path)
return nil
})
if err != nil {
return files, fmt.Errorf("failed to get files in directory: %w", err)
}
return files, nil
}
func cleanFilepaths(paths []string, prefix string) []string {
if prefix == "./" {
// if prefix is relative, no prefix and ./ is the same thing, ignore
return paths
}
result := make([]string, len(paths))
for i, p := range paths {
result[i] = prefix + p
}
return result
}