Skip to content

Commit

Permalink
feat: 🎸 maxMatches to limit matches and exit early
Browse files Browse the repository at this point in the history
This introduces a new option `maxMatches` which can be used to limit the
number of matches that fast-glob returns.

Closes: mrmlnc#225
  • Loading branch information
joscha committed Nov 5, 2019
1 parent 24a29c5 commit 4d06b65
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 1 deletion.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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`
Expand Down
1 change: 1 addition & 0 deletions src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default abstract class Provider<T> {
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()
Expand Down
45 changes: 45 additions & 0 deletions src/readers/stream.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
});
});
13 changes: 12 additions & 1 deletion src/readers/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,26 @@ export default class ReaderStream extends Reader<NodeJS.ReadableStream> {
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();
}

Expand Down
33 changes: 33 additions & 0 deletions src/readers/sync.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
});
4 changes: 4 additions & 0 deletions src/readers/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default class ReaderSync extends Reader<Entry[]> {

public static(patterns: Pattern[], options: ReaderOptions): Entry[] {
const entries: Entry[] = [];
let matches = 0;

for (const pattern of patterns) {
const filepath = this._getFullEntryPath(pattern);
Expand All @@ -26,6 +27,9 @@ export default class ReaderSync extends Reader<Entry[]> {
}

entries.push(entry);
if (options.maxMatches === ++matches) {
break;
}
}

return entries;
Expand Down
9 changes: 9 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions src/tests/smoke/max-matches.smoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as smoke from './smoke';

smoke.suite('Smoke → MarkDirectories', [
{
pattern: 'fixtures/**/*',
fgOptions: { maxMatches: 1 }
}
]);
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type ReaderOptions = fsWalk.Options & {
entryFilter: EntryFilterFunction;
errorFilter: ErrorFilterFunction;
fs: FileSystemAdapter;
maxMatches: number;
stats: boolean;
};

Expand Down

0 comments on commit 4d06b65

Please sign in to comment.