Skip to content

Commit 1273541

Browse files
committed
Support negation-only patterns
Fixes #237
1 parent c2eb272 commit 1273541

File tree

5 files changed

+65
-6
lines changed

5 files changed

+65
-6
lines changed

index.d.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ Find files and directories using glob patterns.
150150
151151
Note that glob patterns can only contain forward-slashes, not backward-slashes, so if you want to construct a glob pattern from path components, you need to use `path.posix.join()` instead of `path.join()`.
152152
153-
@param patterns - See the supported [glob patterns](https://github.com/sindresorhus/globby#globbing-patterns).
153+
@param patterns - See the supported [glob patterns](https://github.com/sindresorhus/globby#globbing-patterns). Supports negation patterns to exclude files. When using only negation patterns (like `['!*.json']`), globby implicitly prepends a catch-all pattern to match all files before applying negations.
154154
@param options - See the [`fast-glob` options](https://github.com/mrmlnc/fast-glob#options-3) in addition to the ones in this package.
155155
@returns The matching paths.
156156
@@ -163,6 +163,17 @@ const paths = await globby(['*', '!cake']);
163163
console.log(paths);
164164
//=> ['unicorn', 'rainbow']
165165
```
166+
167+
@example
168+
```
169+
import {globby} from 'globby';
170+
171+
// Negation-only patterns match all files except the negated ones
172+
const paths = await globby(['!*.json', '!*.xml'], {cwd: 'config'});
173+
174+
console.log(paths);
175+
//=> ['config.js', 'settings.yaml']
176+
```
166177
*/
167178
export function globby(
168179
patterns: string | readonly string[],

index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,12 @@ const createFilterFunction = isIgnored => {
269269
const unionFastGlobResults = (results, filter) => results.flat().filter(fastGlobResult => filter(fastGlobResult));
270270

271271
const convertNegativePatterns = (patterns, options) => {
272+
// If all patterns are negative, prepend a positive catch-all pattern
273+
// This makes negation-only patterns work intuitively (e.g., '!*.json' matches all files except JSON)
274+
if (patterns.length > 0 && patterns.every(pattern => isNegativePattern(pattern))) {
275+
patterns = ['**/*', ...patterns];
276+
}
277+
272278
const tasks = [];
273279

274280
while (patterns.length > 0) {

readme.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Based on [`fast-glob`](https://github.com/mrmlnc/fast-glob) but adds a bunch of
99
- Promise API
1010
- Multiple patterns
1111
- Negated patterns: `['foo*', '!foobar']`
12+
- Negation-only patterns: `['!foobar']` → matches all files except `foobar`
1213
- Expands directories: `foo``foo/**/*`
1314
- Supports `.gitignore` and similar ignore config files
1415
- Supports `URL` as `cwd`
@@ -332,6 +333,26 @@ Just a quick overview.
332333
- `{}` allows for a comma-separated list of "or" expressions
333334
- `!` at the beginning of a pattern will negate the match
334335

336+
### Negation patterns
337+
338+
Globby supports negation patterns to exclude files. There are two ways to use them:
339+
340+
**With positive patterns:**
341+
```js
342+
await globby(['src/**/*.js', '!src/**/*.test.js']);
343+
// Matches all .js files except test files
344+
```
345+
346+
**Negation-only patterns:**
347+
```js
348+
await globby(['!*.json', '!*.xml'], {cwd: 'config'});
349+
// Matches all files in config/ except .json and .xml files
350+
```
351+
352+
When using only negation patterns, globby implicitly prepends `**/*` to match all files, then applies the negations. This means `['!*.json', '!*.xml']` is equivalent to `['**/*', '!*.json', '!*.xml']`.
353+
354+
**Note:** The prepended `**/*` pattern respects the `dot` option. By default, dotfiles (files starting with `.`) are not matched unless you set `dot: true`.
355+
335356
[Various patterns and expected matches.](https://github.com/sindresorhus/multimatch/blob/main/test/test.js)
336357

337358
## Related

tests/generate-glob-tasks.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ test('combine tasks', async t => {
121121

122122
t.deepEqual(
123123
await getTasks(t, ['!a']),
124-
[],
124+
[{patterns: ['**/*'], ignore: ['a']}],
125125
);
126126

127127
t.deepEqual(
@@ -209,15 +209,25 @@ test('random patterns', async t => {
209209
const allPatterns = tasks.flatMap(({patterns}) => patterns);
210210
const allIgnore = tasks.flatMap(({ignore}) => ignore);
211211

212+
// When there are only negative patterns, we auto-add '**/*' as a positive pattern
213+
const isNegationOnly = positivePatterns.length === 0 && negativePatterns.length > 0;
214+
const expectedPatternCount = isNegationOnly ? 1 : positivePatterns.length;
215+
212216
t.is(
213217
new Set(allPatterns).size,
214-
positivePatterns.length,
218+
expectedPatternCount,
215219
`positive patterns should be in patterns: ${patternsToDebug}`,
216220
);
217221

222+
// When there are only negative patterns, all of them go into ignore (including negativePatternsAtStart)
223+
// Otherwise, negativePatternsAtStart are discarded
224+
const expectedIgnoreCount = isNegationOnly
225+
? negativePatterns.length
226+
: negativePatterns.length - negativePatternsAtStart.length;
227+
218228
t.is(
219229
new Set(allIgnore).size,
220-
negativePatterns.length - negativePatternsAtStart.length,
230+
expectedIgnoreCount,
221231
`negative patterns should be in ignore: ${patternsToDebug}`,
222232
);
223233
}

tests/globby.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,19 @@ test('respect patterns order', async t => {
233233
t.deepEqual(await runGlobby(t, ['!*.tmp', 'a.tmp']), ['a.tmp']);
234234
});
235235

236-
test('return [] for all negative patterns', async t => {
237-
t.deepEqual(await runGlobby(t, ['!a.tmp', '!b.tmp']), []);
236+
test('negation-only patterns match all files in cwd except negated ones', async t => {
237+
// When using negation-only patterns in a scoped directory, it should match all files except the negated ones
238+
t.deepEqual(await runGlobby(t, ['!a.tmp', '!b.tmp'], {cwd: temporary}), ['c.tmp', 'd.tmp', 'e.tmp']);
239+
});
240+
241+
test('single negation-only pattern in scoped directory', async t => {
242+
const result = await runGlobby(t, '!a.tmp', {cwd: temporary});
243+
t.deepEqual(result, ['b.tmp', 'c.tmp', 'd.tmp', 'e.tmp']);
244+
});
245+
246+
test('negation-only with brace expansion in scoped directory', async t => {
247+
const result = await runGlobby(t, '!{a,b}.tmp', {cwd: temporary});
248+
t.deepEqual(result, ['c.tmp', 'd.tmp', 'e.tmp']);
238249
});
239250

240251
test('glob - stream async iterator support', async t => {

0 commit comments

Comments
 (0)