diff --git a/README.md b/README.md index 1a8173cb..184ceac4 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,16 @@ paths](#windows). For legacy reasons, this is also set if `options.allowWindowsEscape` is set to the exact value `false`. +### windowsNoMagicRoot + +When a pattern starts with a UNC path or drive letter, and in +`nocase:true` mode, do not convert the root portions of the +pattern into a case-insensitive regular expression, and instead +leave them as strings. + +This is the default when the platform is `win32` and +`nocase:true` is set. + ### preserveMultipleSlashes By default, multiple `/` characters (other than the leading `//` diff --git a/changelog.md b/changelog.md index b9f00ffa..b7048bc2 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # change log +## 7.2 + +- Add `windowsNoMagicRoot` option + ## 7.1 - Add `optimizationLevel` configuration option, and revert the diff --git a/src/index.ts b/src/index.ts index 5492d837..b14c22c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ export interface MinimatchOptions { preserveMultipleSlashes?: boolean optimizationLevel?: number platform?: typeof process.platform + windowsNoMagicRoot?: boolean } export const minimatch = ( @@ -272,6 +273,7 @@ minimatch.match = match // replace stuff like \* with * const globUnescape = (s: string) => s.replace(/\\(.)/g, '$1') +const globMagic = /[?*]|[+@!]\(.*?\)|\[|\]/ const charUnescape = (s: string) => s.replace(/\\([^-\]])/g, '$1') const regExpEscape = (s: string) => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') @@ -311,9 +313,11 @@ export class Minimatch { partial: boolean globSet: string[] globParts: string[][] + nocase: boolean isWindows: boolean platform: typeof process.platform + windowsNoMagicRoot: boolean regexp: false | null | MMRegExp constructor(pattern: string, options: MinimatchOptions = {}) { @@ -336,6 +340,11 @@ export class Minimatch { this.comment = false this.empty = false this.partial = !!options.partial + this.nocase = !!this.options.nocase + this.windowsNoMagicRoot = + options.windowsNoMagicRoot !== undefined + ? options.windowsNoMagicRoot + : !!(this.isWindows && this.nocase) this.globSet = [] this.globParts = [] @@ -388,7 +397,23 @@ export class Minimatch { this.debug(this.pattern, this.globParts) // glob --> regexps - let set = this.globParts.map((s, _, __) => s.map(ss => this.parse(ss))) + let set = this.globParts.map((s, _, __) => { + if (this.isWindows && this.windowsNoMagicRoot) { + // check if it's a drive or unc path. + const isUNC = + s[0] === '' && + s[1] === '' && + (s[2] === '?' || !globMagic.test(s[2])) && + !globMagic.test(s[3]) + const isDrive = /^[a-z]:/i.test(s[0]) + if (isUNC) { + return [...s.slice(0, 4), ...s.slice(4).map(ss => this.parse(ss))] + } else if (isDrive) { + return [s[0], ...s.slice(1).map(ss => this.parse(ss))] + } + } + return s.map(ss => this.parse(ss)) + }) this.debug(this.pattern, set) diff --git a/tap-snapshots/test/windows-no-magic-root.ts.test.cjs b/tap-snapshots/test/windows-no-magic-root.ts.test.cjs new file mode 100644 index 00000000..fd2a829d --- /dev/null +++ b/tap-snapshots/test/windows-no-magic-root.ts.test.cjs @@ -0,0 +1,210 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/windows-no-magic-root.ts TAP no magic the root //?/d: > default to true 1`] = ` +Array [ + Array [ + "", + "", + "?", + "d:", + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root //?/d: > set explicitly false 1`] = ` +Array [ + Array [ + "", + "", + /^(?!\\.)(?=.)[^/]$/i, + /^d:$/i, + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root //?/d:/ > default to true 1`] = ` +Array [ + Array [ + "", + "", + "?", + "d:", + "", + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root //?/d:/ > set explicitly false 1`] = ` +Array [ + Array [ + "", + "", + /^(?!\\.)(?=.)[^/]$/i, + /^d:$/i, + "", + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root //?/d:/x/y/z > default to true 1`] = ` +Array [ + Array [ + "", + "", + "?", + "d:", + /^x$/i, + /^y$/i, + /^z$/i, + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root //?/d:/x/y/z > set explicitly false 1`] = ` +Array [ + Array [ + "", + "", + /^(?!\\.)(?=.)[^/]$/i, + /^d:$/i, + /^x$/i, + /^y$/i, + /^z$/i, + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root //host/share > default to true 1`] = ` +Array [ + Array [ + "", + "", + "host", + "share", + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root //host/share > set explicitly false 1`] = ` +Array [ + Array [ + "", + "", + /^host$/i, + /^share$/i, + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root //host/share/ > default to true 1`] = ` +Array [ + Array [ + "", + "", + "host", + "share", + "", + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root //host/share/ > set explicitly false 1`] = ` +Array [ + Array [ + "", + "", + /^host$/i, + /^share$/i, + "", + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root //host/share/x/y/z > default to true 1`] = ` +Array [ + Array [ + "", + "", + "host", + "share", + /^x$/i, + /^y$/i, + /^z$/i, + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root //host/share/x/y/z > set explicitly false 1`] = ` +Array [ + Array [ + "", + "", + /^host$/i, + /^share$/i, + /^x$/i, + /^y$/i, + /^z$/i, + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root d: > default to true 1`] = ` +Array [ + Array [ + "d:", + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root d: > set explicitly false 1`] = ` +Array [ + Array [ + /^d:$/i, + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root d:/ > default to true 1`] = ` +Array [ + Array [ + "d:", + "", + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root d:/ > set explicitly false 1`] = ` +Array [ + Array [ + /^d:$/i, + "", + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root d:/x/y/z > default to true 1`] = ` +Array [ + Array [ + "d:", + /^x$/i, + /^y$/i, + /^z$/i, + ], +] +` + +exports[`test/windows-no-magic-root.ts TAP no magic the root d:/x/y/z > set explicitly false 1`] = ` +Array [ + Array [ + /^d:$/i, + /^x$/i, + /^y$/i, + /^z$/i, + ], +] +` diff --git a/test/windows-no-magic-root.ts b/test/windows-no-magic-root.ts new file mode 100644 index 00000000..3d4a62d3 --- /dev/null +++ b/test/windows-no-magic-root.ts @@ -0,0 +1,31 @@ +import {Minimatch} from '../' +import t from 'tap' + +t.test('no magic the root', t => { + const patterns = [ + '//host/share/x/y/z', + '//host/share/', + '//host/share', + '//?/d:/x/y/z', + '//?/d:/', + '//?/d:', + 'd:/x/y/z', + 'd:/', + 'd:', + ] + t.plan(patterns.length) + for (const p of patterns) { + t.test(p, t => { + t.matchSnapshot(new Minimatch(p, { + platform: 'win32', + nocase: true, + }).set, 'default to true') + t.matchSnapshot(new Minimatch(p, { + windowsNoMagicRoot: false, + platform: 'win32', + nocase: true, + }).set, 'set explicitly false') + t.end() + }) + } +})