picomatch.js 9.7 KB

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