picomatch.js 10 KB

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