blob: df7438a94f826aa070313f7ae16b21a70a493a60 [file] [log] [blame]
Leo Repp58b9f112021-11-22 11:57:47 +01001'use strict';
2
3const path = require('path');
4const scan = require('./scan');
5const parse = require('./parse');
6const utils = require('./utils');
7const constants = require('./constants');
8const isObject = val => val && typeof val === 'object' && !Array.isArray(val);
9
10/**
11 * Creates a matcher function from one or more glob patterns. The
12 * returned function takes a string to match as its first argument,
13 * and returns true if the string is a match. The returned matcher
14 * function also takes a boolean as the second argument that, when true,
15 * returns an object with additional information.
16 *
17 * ```js
18 * const picomatch = require('picomatch');
19 * // picomatch(glob[, options]);
20 *
21 * const isMatch = picomatch('*.!(*a)');
22 * console.log(isMatch('a.a')); //=> false
23 * console.log(isMatch('a.b')); //=> true
24 * ```
25 * @name picomatch
26 * @param {String|Array} `globs` One or more glob patterns.
27 * @param {Object=} `options`
28 * @return {Function=} Returns a matcher function.
29 * @api public
30 */
31
32const picomatch = (glob, options, returnState = false) => {
33 if (Array.isArray(glob)) {
34 const fns = glob.map(input => picomatch(input, options, returnState));
35 const arrayMatcher = str => {
36 for (const isMatch of fns) {
37 const state = isMatch(str);
38 if (state) return state;
39 }
40 return false;
41 };
42 return arrayMatcher;
43 }
44
45 const isState = isObject(glob) && glob.tokens && glob.input;
46
47 if (glob === '' || (typeof glob !== 'string' && !isState)) {
48 throw new TypeError('Expected pattern to be a non-empty string');
49 }
50
51 const opts = options || {};
52 const posix = utils.isWindows(options);
53 const regex = isState
54 ? picomatch.compileRe(glob, options)
55 : picomatch.makeRe(glob, options, false, true);
56
57 const state = regex.state;
58 delete regex.state;
59
60 let isIgnored = () => false;
61 if (opts.ignore) {
62 const ignoreOpts = { ...options, ignore: null, onMatch: null, onResult: null };
63 isIgnored = picomatch(opts.ignore, ignoreOpts, returnState);
64 }
65
66 const matcher = (input, returnObject = false) => {
67 const { isMatch, match, output } = picomatch.test(input, regex, options, { glob, posix });
68 const result = { glob, state, regex, posix, input, output, match, isMatch };
69
70 if (typeof opts.onResult === 'function') {
71 opts.onResult(result);
72 }
73
74 if (isMatch === false) {
75 result.isMatch = false;
76 return returnObject ? result : false;
77 }
78
79 if (isIgnored(input)) {
80 if (typeof opts.onIgnore === 'function') {
81 opts.onIgnore(result);
82 }
83 result.isMatch = false;
84 return returnObject ? result : false;
85 }
86
87 if (typeof opts.onMatch === 'function') {
88 opts.onMatch(result);
89 }
90 return returnObject ? result : true;
91 };
92
93 if (returnState) {
94 matcher.state = state;
95 }
96
97 return matcher;
98};
99
100/**
101 * Test `input` with the given `regex`. This is used by the main
102 * `picomatch()` function to test the input string.
103 *
104 * ```js
105 * const picomatch = require('picomatch');
106 * // picomatch.test(input, regex[, options]);
107 *
108 * console.log(picomatch.test('foo/bar', /^(?:([^/]*?)\/([^/]*?))$/));
109 * // { isMatch: true, match: [ 'foo/', 'foo', 'bar' ], output: 'foo/bar' }
110 * ```
111 * @param {String} `input` String to test.
112 * @param {RegExp} `regex`
113 * @return {Object} Returns an object with matching info.
114 * @api public
115 */
116
117picomatch.test = (input, regex, options, { glob, posix } = {}) => {
118 if (typeof input !== 'string') {
119 throw new TypeError('Expected input to be a string');
120 }
121
122 if (input === '') {
123 return { isMatch: false, output: '' };
124 }
125
126 const opts = options || {};
127 const format = opts.format || (posix ? utils.toPosixSlashes : null);
128 let match = input === glob;
129 let output = (match && format) ? format(input) : input;
130
131 if (match === false) {
132 output = format ? format(input) : input;
133 match = output === glob;
134 }
135
136 if (match === false || opts.capture === true) {
137 if (opts.matchBase === true || opts.basename === true) {
138 match = picomatch.matchBase(input, regex, options, posix);
139 } else {
140 match = regex.exec(output);
141 }
142 }
143
144 return { isMatch: Boolean(match), match, output };
145};
146
147/**
148 * Match the basename of a filepath.
149 *
150 * ```js
151 * const picomatch = require('picomatch');
152 * // picomatch.matchBase(input, glob[, options]);
153 * console.log(picomatch.matchBase('foo/bar.js', '*.js'); // true
154 * ```
155 * @param {String} `input` String to test.
156 * @param {RegExp|String} `glob` Glob pattern or regex created by [.makeRe](#makeRe).
157 * @return {Boolean}
158 * @api public
159 */
160
161picomatch.matchBase = (input, glob, options, posix = utils.isWindows(options)) => {
162 const regex = glob instanceof RegExp ? glob : picomatch.makeRe(glob, options);
163 return regex.test(path.basename(input));
164};
165
166/**
167 * Returns true if **any** of the given glob `patterns` match the specified `string`.
168 *
169 * ```js
170 * const picomatch = require('picomatch');
171 * // picomatch.isMatch(string, patterns[, options]);
172 *
173 * console.log(picomatch.isMatch('a.a', ['b.*', '*.a'])); //=> true
174 * console.log(picomatch.isMatch('a.a', 'b.*')); //=> false
175 * ```
176 * @param {String|Array} str The string to test.
177 * @param {String|Array} patterns One or more glob patterns to use for matching.
178 * @param {Object} [options] See available [options](#options).
179 * @return {Boolean} Returns true if any patterns match `str`
180 * @api public
181 */
182
183picomatch.isMatch = (str, patterns, options) => picomatch(patterns, options)(str);
184
185/**
186 * Parse a glob pattern to create the source string for a regular
187 * expression.
188 *
189 * ```js
190 * const picomatch = require('picomatch');
191 * const result = picomatch.parse(pattern[, options]);
192 * ```
193 * @param {String} `pattern`
194 * @param {Object} `options`
195 * @return {Object} Returns an object with useful properties and output to be used as a regex source string.
196 * @api public
197 */
198
199picomatch.parse = (pattern, options) => {
200 if (Array.isArray(pattern)) return pattern.map(p => picomatch.parse(p, options));
201 return parse(pattern, { ...options, fastpaths: false });
202};
203
204/**
205 * Scan a glob pattern to separate the pattern into segments.
206 *
207 * ```js
208 * const picomatch = require('picomatch');
209 * // picomatch.scan(input[, options]);
210 *
211 * const result = picomatch.scan('!./foo/*.js');
212 * console.log(result);
213 * { prefix: '!./',
214 * input: '!./foo/*.js',
215 * start: 3,
216 * base: 'foo',
217 * glob: '*.js',
218 * isBrace: false,
219 * isBracket: false,
220 * isGlob: true,
221 * isExtglob: false,
222 * isGlobstar: false,
223 * negated: true }
224 * ```
225 * @param {String} `input` Glob pattern to scan.
226 * @param {Object} `options`
227 * @return {Object} Returns an object with
228 * @api public
229 */
230
231picomatch.scan = (input, options) => scan(input, options);
232
233/**
234 * Create a regular expression from a parsed glob pattern.
235 *
236 * ```js
237 * const picomatch = require('picomatch');
238 * const state = picomatch.parse('*.js');
239 * // picomatch.compileRe(state[, options]);
240 *
241 * console.log(picomatch.compileRe(state));
242 * //=> /^(?:(?!\.)(?=.)[^/]*?\.js)$/
243 * ```
244 * @param {String} `state` The object returned from the `.parse` method.
245 * @param {Object} `options`
246 * @return {RegExp} Returns a regex created from the given pattern.
247 * @api public
248 */
249
250picomatch.compileRe = (parsed, options, returnOutput = false, returnState = false) => {
251 if (returnOutput === true) {
252 return parsed.output;
253 }
254
255 const opts = options || {};
256 const prepend = opts.contains ? '' : '^';
257 const append = opts.contains ? '' : '$';
258
259 let source = `${prepend}(?:${parsed.output})${append}`;
260 if (parsed && parsed.negated === true) {
261 source = `^(?!${source}).*$`;
262 }
263
264 const regex = picomatch.toRegex(source, options);
265 if (returnState === true) {
266 regex.state = parsed;
267 }
268
269 return regex;
270};
271
272picomatch.makeRe = (input, options, returnOutput = false, returnState = false) => {
273 if (!input || typeof input !== 'string') {
274 throw new TypeError('Expected a non-empty string');
275 }
276
277 const opts = options || {};
278 let parsed = { negated: false, fastpaths: true };
279 let prefix = '';
280 let output;
281
282 if (input.startsWith('./')) {
283 input = input.slice(2);
284 prefix = parsed.prefix = './';
285 }
286
287 if (opts.fastpaths !== false && (input[0] === '.' || input[0] === '*')) {
288 output = parse.fastpaths(input, options);
289 }
290
291 if (output === undefined) {
292 parsed = parse(input, options);
293 parsed.prefix = prefix + (parsed.prefix || '');
294 } else {
295 parsed.output = output;
296 }
297
298 return picomatch.compileRe(parsed, options, returnOutput, returnState);
299};
300
301/**
302 * Create a regular expression from the given regex source string.
303 *
304 * ```js
305 * const picomatch = require('picomatch');
306 * // picomatch.toRegex(source[, options]);
307 *
308 * const { output } = picomatch.parse('*.js');
309 * console.log(picomatch.toRegex(output));
310 * //=> /^(?:(?!\.)(?=.)[^/]*?\.js)$/
311 * ```
312 * @param {String} `source` Regular expression source string.
313 * @param {Object} `options`
314 * @return {RegExp}
315 * @api public
316 */
317
318picomatch.toRegex = (source, options) => {
319 try {
320 const opts = options || {};
321 return new RegExp(source, opts.flags || (opts.nocase ? 'i' : ''));
322 } catch (err) {
323 if (options && options.debug === true) throw err;
324 return /$^/;
325 }
326};
327
328/**
329 * Picomatch constants.
330 * @return {Object}
331 */
332
333picomatch.constants = constants;
334
335/**
336 * Expose "picomatch"
337 */
338
339module.exports = picomatch;