diff --git a/README.md b/README.md index f698d58a..78b9ec4d 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ This package provides methods for traversing the file system and returning pathn * [Output control](#output-control) * [absolute](#absolute) * [markDirectories](#markdirectories) + * [maxMatches](#maxmatches) * [objectMode](#objectmode) * [onlyDirectories](#onlydirectories) * [onlyFiles](#onlyfiles) @@ -419,6 +420,18 @@ fs.sync('*', { onlyFiles: false, markDirectories: false }); // ['index.js', 'con fs.sync('*', { onlyFiles: false, markDirectories: true }); // ['index.js', 'controllers/'] ``` +#### maxMatches + +* Type: `number` +* Default: `Infinity` + +Limits the number of matches. + +```js +fs.sync('*', { maxMatches: Infinity }); // ['a.js', 'b.js', ...] +fs.sync('*', { maxMatches: 1 }); // ['a.js'] +``` + #### objectMode * Type: `boolean` diff --git a/src/providers/provider.ts b/src/providers/provider.ts index 5e293f0b..a1f41888 100644 --- a/src/providers/provider.ts +++ b/src/providers/provider.ts @@ -34,6 +34,7 @@ export default abstract class Provider { errorFilter: this.errorFilter.getFilter(), followSymbolicLinks: this._settings.followSymbolicLinks, fs: this._settings.fs, + maxMatches: this._settings.maxMatches, stats: this._settings.stats, throwErrorOnBrokenSymbolicLink: this._settings.throwErrorOnBrokenSymbolicLink, transform: this.entryTransformer.getTransformer() diff --git a/src/readers/stream.spec.ts b/src/readers/stream.spec.ts index f7312500..482fae8c 100644 --- a/src/readers/stream.spec.ts +++ b/src/readers/stream.spec.ts @@ -137,5 +137,50 @@ describe('Readers → ReaderStream', () => { done(); }); }); + + describe('maxMatches', () => { + it('can be used to limit matches', (done) => { + const reader = getReader(); + const maxMatches = 2; + const readerOptions = getReaderOptions({ + maxMatches, + entryFilter: () => true + }); + + reader.stat.yields(null, new Stats()); + + const entries: Entry[] = []; + + const stream = reader.static(['1.txt', '2.txt', '3.txt'], readerOptions); + + stream.on('data', (entry: Entry) => entries.push(entry)); + stream.once('end', () => { + assert.strictEqual(entries.length, maxMatches); + assert.strictEqual(entries[0].name, '1.txt'); + assert.strictEqual(entries[1].name, '2.txt'); + done(); + }); + }); + + it('is ignored if less or equal than 1', (done) => { + const reader = getReader(); + const readerOptions = getReaderOptions({ + maxMatches: -1, + entryFilter: () => true + }); + + reader.stat.yields(null, new Stats()); + + let matches = 0; + + const stream = reader.static(['1.txt', '2.txt', '3.txt'], readerOptions); + + stream.on('data', () => matches++); + stream.once('end', () => { + assert.strictEqual(matches, 3); + done(); + }); + }); + }); }); }); diff --git a/src/readers/stream.ts b/src/readers/stream.ts index 800b3a3e..4a9e347a 100644 --- a/src/readers/stream.ts +++ b/src/readers/stream.ts @@ -19,15 +19,26 @@ export default class ReaderStream extends Reader { const filepaths = patterns.map(this._getFullEntryPath, this); const stream = new PassThrough({ objectMode: true }); + let matches = 0; stream._write = (index: number, _enc, done) => { + if (options.maxMatches === matches) { + // this is not ideal because we are still passing patterns to write + // even though we know the stream is already finished. We can't use + // .writableEnded either because finding matches is asynchronous + // The best we could do is to await the write inside the for loop below + // however that would mean that this whole function would become async + done(); + return; + } return this._getEntry(filepaths[index], patterns[index], options) .then((entry) => { if (entry !== null && options.entryFilter(entry)) { stream.push(entry); + matches++; } - if (index === filepaths.length - 1) { + if (index === filepaths.length - 1 || options.maxMatches === matches) { stream.end(); } diff --git a/src/readers/sync.spec.ts b/src/readers/sync.spec.ts index 06994a0d..47e023e5 100644 --- a/src/readers/sync.spec.ts +++ b/src/readers/sync.spec.ts @@ -113,5 +113,38 @@ describe('Readers → ReaderSync', () => { assert.strictEqual(actual.length, 0); }); + + describe('maxMatches', () => { + it('can be used to limit matches', () => { + const reader = getReader(); + const maxMatches = 2; + const readerOptions = getReaderOptions({ + maxMatches, + entryFilter: () => true + }); + + reader.statSync.returns(new Stats()); + + const actual = reader.static(['1.txt', '2.txt', '3.txt'], readerOptions); + + assert.strictEqual(actual.length, maxMatches); + assert.strictEqual(actual[0].name, '1.txt'); + assert.strictEqual(actual[1].name, '2.txt'); + }); + + it('is ignored if less or equal than 1', () => { + const reader = getReader(); + const readerOptions = getReaderOptions({ + maxMatches: -1, + entryFilter: () => true + }); + + reader.statSync.returns(new Stats()); + + const actual = reader.static(['1.txt', '2.txt', '3.txt'], readerOptions); + + assert.strictEqual(actual.length, 3); + }); + }); }); }); diff --git a/src/readers/sync.ts b/src/readers/sync.ts index 39096f14..164caeae 100644 --- a/src/readers/sync.ts +++ b/src/readers/sync.ts @@ -16,6 +16,7 @@ export default class ReaderSync extends Reader { public static(patterns: Pattern[], options: ReaderOptions): Entry[] { const entries: Entry[] = []; + let matches = 0; for (const pattern of patterns) { const filepath = this._getFullEntryPath(pattern); @@ -26,6 +27,9 @@ export default class ReaderSync extends Reader { } entries.push(entry); + if (options.maxMatches === ++matches) { + break; + } } return entries; diff --git a/src/settings.ts b/src/settings.ts index 7b8aacb1..a70c4fa9 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -104,6 +104,13 @@ export type Options = { * @default false */ markDirectories?: boolean; + /** + * Exit after having gathered `maxMatches` matches. + * If given, expects a positive number greater or equal to 1. + * + * @default Infinity + */ + maxMatches?: number; /** * Returns objects (instead of strings) describing entries. * @@ -165,6 +172,8 @@ export default class Settings { public readonly globstar: boolean = this._getValue(this._options.globstar, true); public readonly ignore: Pattern[] = this._getValue(this._options.ignore, [] as Pattern[]); public readonly markDirectories: boolean = this._getValue(this._options.markDirectories, false); + // If 0 or negative maxMatches is given, we revert to infinite matches + public readonly maxMatches: number = Math.max(0, this._getValue(this._options.maxMatches, Infinity)) || Infinity; public readonly objectMode: boolean = this._getValue(this._options.objectMode, false); public readonly onlyDirectories: boolean = this._getValue(this._options.onlyDirectories, false); public readonly onlyFiles: boolean = this._getValue(this._options.onlyFiles, true); diff --git a/src/tests/smoke/max-matches.smoke.ts b/src/tests/smoke/max-matches.smoke.ts new file mode 100644 index 00000000..0ecfc0e1 --- /dev/null +++ b/src/tests/smoke/max-matches.smoke.ts @@ -0,0 +1,8 @@ +import * as smoke from './smoke'; + +smoke.suite('Smoke → MarkDirectories', [ + { + pattern: 'fixtures/**/*', + fgOptions: { maxMatches: 1 } + } +]); diff --git a/src/types/index.ts b/src/types/index.ts index 646ed42e..9afd3de4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -15,6 +15,7 @@ export type ReaderOptions = fsWalk.Options & { entryFilter: EntryFilterFunction; errorFilter: ErrorFilterFunction; fs: FileSystemAdapter; + maxMatches: number; stats: boolean; };