diff --git a/README.md b/README.md index 12cb731..2a65dd6 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,9 @@ Returns a EventEmitter reading given root recursively. ### Options -* `pattern`: Glob pattern or Array of Glob patterns to match the found files with. -* `ignore`: Glob pattern or Array of Glob patterns to exclude matches. Note: `ignore` patterns are *always* in `dot:true` mode. +* `pattern`: Glob pattern or Array of Glob patterns to match the found files with. A file has to match at least one of the provided patterns to be returned. +* `ignore`: Glob pattern or Array of Glob patterns to exclude matches. If a file or a folder matches at least one of the provided patterns, it's not returned. It doesn't prevent files from folder content to be returned. Note: `ignore` patterns are *always* in `dot:true` mode. +* `skip`: Glob pattern or Array of Glob patterns to exclude folders. If a folder matches one of the provided patterns, it's not returned, and it's not explored: this prevents any of its children to be returned. Note: `skip` patterns are *always* in `dot:true` mode. * `mark`: Add a `/` character to directory matches. * `stat`: Set to true to stat *all* results. This reduces performance. * `silent`: When an unusual error is encountered when attempting to read a directory, a warning will be printed to stderr. Set the `silent` option to true to suppress these warnings. diff --git a/index.js b/index.js index 2a20edc..fde1a92 100644 --- a/index.js +++ b/index.js @@ -59,7 +59,7 @@ function stat(file, followSyslinks) { }); } -async function* exploreWalkAsync(dir, path, followSyslinks, useStat, strict) { +async function* exploreWalkAsync(dir, path, followSyslinks, useStat, shouldSkip, strict) { let files = await readdir(path + dir, strict); for(const file of files) { const filename = dir + '/' + file.name; @@ -71,15 +71,18 @@ async function* exploreWalkAsync(dir, path, followSyslinks, useStat, strict) { } stats = stats || file; - yield {relative, absolute, stats}; - if(stats.isDirectory()) { - yield* exploreWalkAsync(filename, path, followSyslinks, useStat, false); + if(!shouldSkip(relative)) { + yield {relative, absolute, stats}; + yield* exploreWalkAsync(filename, path, followSyslinks, useStat, shouldSkip, false); + } + } else { + yield {relative, absolute, stats}; } } } -async function* explore(path, followSyslinks, useStat) { - yield* exploreWalkAsync('', path, followSyslinks, useStat, true); +async function* explore(path, followSyslinks, useStat, shouldSkip) { + yield* exploreWalkAsync('', path, followSyslinks, useStat, shouldSkip, true); } @@ -91,6 +94,7 @@ function readOptions(options) { matchBase: !!options.matchBase, nocase: !!options.nocase, ignore: options.ignore, + skip: options.skip, follow: !!options.follow, stat: !!options.stat, @@ -114,7 +118,7 @@ class ReaddirGlob extends EventEmitter { if(this.options.pattern) { const matchers = Array.isArray(this.options.pattern) ? this.options.pattern : [this.options.pattern]; this.matchers = matchers.map( m => - new Minimatch(this.options.pattern, { + new Minimatch(m, { dot: this.options.dot, noglobstar:this.options.noglobstar, matchBase:this.options.matchBase, @@ -130,8 +134,16 @@ class ReaddirGlob extends EventEmitter { new Minimatch(ignore, {dot: true}) ); } + + this.skipMatchers = []; + if(this.options.skip) { + const skipPatterns = Array.isArray(this.options.skip) ? this.options.skip : [this.options.skip]; + this.skipMatchers = skipPatterns.map( skip => + new Minimatch(skip, {dot: true}) + ); + } - this.iterator = explore(resolve(cwd || '.'), this.options.follow, this.options.stat); + this.iterator = explore(resolve(cwd || '.'), this.options.follow, this.options.stat, this._shouldSkipDirectory.bind(this)); this.paused = false; this.inactive = false; this.aborted = false; @@ -146,9 +158,14 @@ class ReaddirGlob extends EventEmitter { setTimeout( () => this._next(), 0); } + _shouldSkipDirectory(relative) { + //console.log(relative, this.skipMatchers.some(m => m.match(relative))); + return this.skipMatchers.some(m => m.match(relative)); + } + _fileMatches(relative, isDirectory) { const file = relative + (isDirectory ? '/' : ''); - return this.matchers.every(m => m.match(file)) + return (this.matchers.length === 0 || this.matchers.some(m => m.match(file))) && !this.ignoreMatchers.some(m => m.match(file)) && (!this.options.nodir || !isDirectory); } diff --git a/package.json b/package.json index ef623b8..cd44630 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Yann Armelin", "name": "readdir-glob", "description": "Recursive fs.readdir with streaming API and glob filtering.", - "version": "1.0.0", + "version": "1.1.0", "homepage": "https://github.com/Yqnn/node-readdir-glob", "repository": { "type": "git", diff --git a/scripts/benchmark.sh b/scripts/benchmark.sh index ea1d0b8..f140e71 100755 --- a/scripts/benchmark.sh +++ b/scripts/benchmark.sh @@ -25,7 +25,7 @@ fi echo -echo Node statSync and readdirSync timing: +echo Node statSync and readdirSync: time node -e ' var fs=require("fs"); var count = 0; @@ -42,7 +42,7 @@ time node -e ' console.log(count)' echo -echo Node glob timing: +echo readdir-glob: time node -e ' var glob=require(process.argv[1]); glob(".", {nodir: true} ,function (er, files) { @@ -50,7 +50,7 @@ time node -e ' })' "$wd" echo -echo Node glob with pattern timing: +echo readdir-glob with pattern: time node -e ' var glob=require(process.argv[1]); glob(".", {pattern:"**/*.txt"}, function (er, files) { @@ -58,7 +58,7 @@ time node -e ' })' "$wd" echo -echo Node glob with pattern and stat timing: +echo readdir-glob with pattern and stat: time node -e ' var glob=require(process.argv[1]); glob(".", {stat:true, pattern:"**/*.txt"}, function (er, files) { @@ -66,7 +66,7 @@ time node -e ' })' "$wd" echo -echo Node glob with pattern and follow timing: +echo readdir-glob with pattern and follow: time node -e ' var glob=require(process.argv[1]); glob(".", {follow:true, pattern:"**/*.txt"}, function (er, files) { @@ -74,7 +74,7 @@ time node -e ' })' "$wd" echo -echo Node glob with --prof +echo readdir-glob with --prof cd $wd bash ./scripts/profile.sh diff --git a/test/00-env.js b/test/00-env.js index 84597f7..83022b6 100644 --- a/test/00-env.js +++ b/test/00-env.js @@ -1,4 +1,4 @@ const origCwd = process.cwd(); afterAll(() => { - process.chdir(origCwd); // not sure it's needed + process.chdir(origCwd); }); \ No newline at end of file diff --git a/test/new-glob-optional-options.js b/test/new-glob-optional-options.js index f0b86c6..ef69de2 100644 --- a/test/new-glob-optional-options.js +++ b/test/new-glob-optional-options.js @@ -2,13 +2,14 @@ const { ReaddirGlob } = require('..'); const path = require('path'); - -let f = 'test/'+path.basename(__filename); +beforeAll(() => { + process.chdir(__dirname + '/fixtures'); +}); test('new glob, with cb, and no options', done => { - new ReaddirGlob('.', {pattern:f}, function(er, results) { + new ReaddirGlob('./a/bc/e/', function(er, results) { expect(er).toBeFalsy(); - expect(results).toEqual([f]); + expect(results).toEqual(['f']); done(); }); }); diff --git a/test/pattern-list.js b/test/pattern-list.js new file mode 100644 index 0000000..360a404 --- /dev/null +++ b/test/pattern-list.js @@ -0,0 +1,29 @@ +const glob = require('../'); + +beforeAll(() => { + process.chdir(__dirname + '/fixtures'); +}); + + +// [cwd, options, expected] +const cases = [ + [ 'a', { pattern:['abcdef/*', 'z'], mark: true}, [ + "abcdef/g/", + "z/", + ] + ] +]; + +cases.forEach(c => { + const cwd = c[0]; + const options = c[1]; + const expected = c[2].sort(); + test(cwd + ' ' + JSON.stringify(options), done => { + glob(cwd, options, (er, res) => { + expect(er).toBeFalsy(); + res.sort(); + expect(res).toEqual(expected); + done(); + }) + }) +}) diff --git a/test/skip.js b/test/skip.js new file mode 100644 index 0000000..ae37671 --- /dev/null +++ b/test/skip.js @@ -0,0 +1,49 @@ +const glob = require('../'); + +beforeAll(() => { + process.chdir(__dirname + '/fixtures'); +}); + + +// [cwd, options, expected] +const cases = [ + [ 'a', { pattern:'**/*', mark: true, skip: ['*/g', 'cb'] }, [ + "abcdef/", + "abcfed/", + "b/", + "b/c/", + "b/c/d", + "bc/", + "bc/e/", + "bc/e/f", + "c/", + "c/d/", + "c/d/c/", + "c/d/c/b", + "symlink/", + "symlink/a/", + "symlink/a/b/", + "symlink/a/b/c", + "x/", + "z/", + ] + ], + [ 'a/c', { mark: true, skip: '**/c' }, [ + 'd/', + ] + ] +]; + +cases.forEach(c => { + const cwd = c[0]; + const options = c[1]; + const expected = c[2].sort(); + test(cwd + ' ' + JSON.stringify(options), done => { + glob(cwd, options, (er, res) => { + expect(er).toBeFalsy(); + res.sort(); + expect(res).toEqual(expected); + done(); + }) + }) +})