From 4be1093bf92f61b83809eafe0a3a1c094193475e Mon Sep 17 00:00:00 2001 From: MildlyMeticulous <302576729+MildlyMeticulous@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:14:08 +0100 Subject: [PATCH] Treat a leading "!" in a bracket expression as negation by default The conversion of "[!" to "[^" was gated on options.posix, so with the default options "[!abc]" emitted "!" as an ordinary class member and the expression matched exactly the characters it should exclude. "[!...]" is Bash pattern negation rather than a POSIX bracket expression, and "[[:alpha:]]" already works without the option, so the flag was not gating what its name suggests. "[^...]" negation was already unconditional, so the two negation forms disagreed with each other by default. Removing the gate leaves all 1977 existing tests passing and takes agreement with minimatch on a 462-case bracket matrix from 376 to 441. --- lib/parse.js | 2 +- test/brackets.js | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index a85bb2d4..f4b62c8d 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -748,7 +748,7 @@ const parse = (input, options) => { value = `\\${value}`; } - if (opts.posix === true && value === '!' && prev.value === '[') { + if (value === '!' && prev.value === '[') { value = '^'; } diff --git a/test/brackets.js b/test/brackets.js index c5106254..2b78cc4f 100644 --- a/test/brackets.js +++ b/test/brackets.js @@ -23,4 +23,44 @@ describe('brackets', () => { assert(!isMatch('a/b', '[a]*')); }); }); + describe('negation with "!"', () => { + it('should negate a bracket expression with a leading "!"', () => { + assert(!isMatch('a', '[!abc]')); + assert(!isMatch('b', '[!abc]')); + assert(!isMatch('c', '[!abc]')); + assert(isMatch('d', '[!abc]')); + assert(isMatch('x', '[!abc]')); + }); + + it('should negate ranges with a leading "!"', () => { + assert(!isMatch('a', '[!a-c]')); + assert(!isMatch('c', '[!a-c]')); + assert(isMatch('d', '[!a-c]')); + }); + + it('should agree with "^" negation', () => { + assert.strictEqual(isMatch('a', '[!abc]'), isMatch('a', '[^abc]')); + assert.strictEqual(isMatch('d', '[!abc]'), isMatch('d', '[^abc]')); + assert.strictEqual(isMatch('a', '[!a-c]'), isMatch('a', '[^a-c]')); + assert.strictEqual(isMatch('d', '[!a-c]'), isMatch('d', '[^a-c]')); + }); + + it('should negate within a larger pattern', () => { + assert(!isMatch('abc', 'a[!b]c')); + assert(isMatch('axc', 'a[!b]c')); + assert(!isMatch('ad', '[!abc]d')); + assert(isMatch('xd', '[!abc]d')); + }); + + it('should treat "!" as a literal when it is not leading', () => { + assert(isMatch('!', '[a!]')); + assert(isMatch('a', '[a!]')); + assert(!isMatch('b', '[a!]')); + }); + + it('should negate a literal "!"', () => { + assert(!isMatch('!', '[!!]')); + assert(isMatch('a', '[!!]')); + }); + }); });