Skip to content

Commit c2482d4

Browse files
committed
fs: add dot option to glob
Adds an opt-in `dot` boolean option to fs.glob, fs.globSync, and fsPromises.glob. When true, `*` and `**` patterns match basenames that begin with `.`, and the walker descends into directories whose names begin with `.`. Defaults to false to preserve current behavior. Signed-off-by: Michael Smith <owlstronaut@github.com>
1 parent 3a7f31c commit c2482d4

3 files changed

Lines changed: 156 additions & 5 deletions

File tree

doc/api/fs.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,6 +1354,9 @@ behavior is similar to `cp dir1/ dir2/`.
13541354
<!-- YAML
13551355
added: v22.0.0
13561356
changes:
1357+
- version: REPLACEME
1358+
pr-url: https://github.com/nodejs/node/pull/99999
1359+
description: Add support for the `dot` option.
13571360
- version: v26.1.0
13581361
pr-url: https://github.com/nodejs/node/pull/62695
13591362
description: Add support for the `followSymlinks` option.
@@ -1380,6 +1383,10 @@ changes:
13801383
* `pattern` {string|string\[]}
13811384
* `options` {Object}
13821385
* `cwd` {string|URL} current working directory. **Default:** `process.cwd()`
1386+
* `dot` {boolean} When `true`, allows `*` and `**` patterns to match
1387+
basenames starting with a period (`.`), and allows the walker to
1388+
descend into directories whose names start with a period. **Default:**
1389+
`false`.
13831390
* `exclude` {Function|string\[]} Function to filter out files/directories or a
13841391
list of glob patterns to be excluded. If a function is provided, return
13851392
`true` to exclude the item, `false` to include it. **Default:** `undefined`.
@@ -3475,6 +3482,9 @@ descriptor. See [`fs.utimes()`][].
34753482
<!-- YAML
34763483
added: v22.0.0
34773484
changes:
3485+
- version: REPLACEME
3486+
pr-url: https://github.com/nodejs/node/pull/99999
3487+
description: Add support for the `dot` option.
34783488
- version: v26.1.0
34793489
pr-url: https://github.com/nodejs/node/pull/62695
34803490
description: Add support for the `followSymlinks` option.
@@ -3502,6 +3512,10 @@ changes:
35023512
35033513
* `options` {Object}
35043514
* `cwd` {string|URL} current working directory. **Default:** `process.cwd()`
3515+
* `dot` {boolean} When `true`, allows `*` and `**` patterns to match
3516+
basenames starting with a period (`.`), and allows the walker to
3517+
descend into directories whose names start with a period. **Default:**
3518+
`false`.
35053519
* `exclude` {Function|string\[]} Function to filter out files/directories or a
35063520
list of glob patterns to be excluded. If a function is provided, return
35073521
`true` to exclude the item, `false` to include it. **Default:** `undefined`.
@@ -6057,6 +6071,9 @@ Synchronous version of [`fs.futimes()`][]. Returns `undefined`.
60576071
<!-- YAML
60586072
added: v22.0.0
60596073
changes:
6074+
- version: REPLACEME
6075+
pr-url: https://github.com/nodejs/node/pull/99999
6076+
description: Add support for the `dot` option.
60606077
- version: v26.1.0
60616078
pr-url: https://github.com/nodejs/node/pull/62695
60626079
description: Add support for the `followSymlinks` option.
@@ -6083,6 +6100,10 @@ changes:
60836100
* `pattern` {string|string\[]}
60846101
* `options` {Object}
60856102
* `cwd` {string|URL} current working directory. **Default:** `process.cwd()`
6103+
* `dot` {boolean} When `true`, allows `*` and `**` patterns to match
6104+
basenames starting with a period (`.`), and allows the walker to
6105+
descend into directories whose names start with a period. **Default:**
6106+
`false`.
60866107
* `exclude` {Function|string\[]} Function to filter out files/directories or a
60876108
list of glob patterns to be excluded. If a function is provided, return
60886109
`true` to exclude the item, `false` to include it. **Default:** `undefined`.

lib/internal/fs/glob.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -336,16 +336,22 @@ class Glob {
336336
#patterns;
337337
#withFileTypes;
338338
#followSymlinks = false;
339+
#dot = false;
339340
#isExcluded = () => false;
340341
constructor(pattern, options = kEmptyObject) {
341342
validateObject(options, 'options');
342-
const { exclude, cwd, followSymlinks, withFileTypes } = options;
343+
const { dot, exclude, cwd, followSymlinks, withFileTypes } = options;
343344
this.#root = toPathIfFileURL(cwd) ?? '.';
344345
if (followSymlinks != null) {
345346
validateBoolean(followSymlinks, 'options.followSymlinks');
346347
this.#followSymlinks = followSymlinks;
347348
}
349+
if (dot != null) {
350+
validateBoolean(dot, 'options.dot');
351+
this.#dot = dot;
352+
}
348353
this.#withFileTypes = !!withFileTypes;
354+
const matcherOpts = { __proto__: null, dot: this.#dot };
349355
if (exclude != null) {
350356
validateStringArrayOrFunction(exclude, 'options.exclude');
351357
if (ArrayIsArray(exclude)) {
@@ -354,7 +360,7 @@ class Glob {
354360
// consistent comparison before instantiating matchers.
355361
const matchers = exclude
356362
.map((pattern) => resolve(this.#root, pattern))
357-
.map((pattern) => createMatcher(pattern));
363+
.map((pattern) => createMatcher(pattern, matcherOpts));
358364
this.#isExcluded = (value) =>
359365
matchers.some((matcher) => matcher.match(value));
360366
this.#results.setup(this.#root, this.#isExcluded);
@@ -370,7 +376,7 @@ class Glob {
370376
validateString(pattern, 'patterns');
371377
patterns = [pattern];
372378
}
373-
this.matchers = ArrayPrototypeMap(patterns, (pattern) => createMatcher(pattern));
379+
this.matchers = ArrayPrototypeMap(patterns, (pattern) => createMatcher(pattern, matcherOpts));
374380
this.#patterns = ArrayPrototypeFlatMap(this.matchers, (matcher) => ArrayPrototypeMap(matcher.set,
375381
(pattern, i) => new Pattern(
376382
pattern,
@@ -595,7 +601,7 @@ class Glob {
595601

596602
const matchesDot = isDot && pattern.test(nextNonGlobIndex, entry.name);
597603

598-
if ((isDot && !matchesDot) ||
604+
if ((!this.#dot && isDot && !matchesDot) ||
599605
(this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) {
600606
continue;
601607
}
@@ -812,7 +818,7 @@ class Glob {
812818

813819
const matchesDot = isDot && pattern.test(nextNonGlobIndex, entry.name);
814820

815-
if ((isDot && !matchesDot) ||
821+
if ((!this.#dot && isDot && !matchesDot) ||
816822
(this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) {
817823
continue;
818824
}

test/parallel/test-fs-glob.mjs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,3 +669,127 @@ describe('globSync - ENOTDIR', function() {
669669
}
670670
});
671671
});
672+
673+
const dotFixtureDir = tmpdir.resolve('dotfixtures');
674+
async function setupDotFixtures() {
675+
const files = [
676+
'lib/visible.js',
677+
'lib/.hidden.js',
678+
'lib/sub/visible.js',
679+
'lib/sub/.hidden.js',
680+
'lib/.dotdir/regular.js',
681+
'lib/.dotdir/sub/deep.js',
682+
];
683+
for (const f of files) {
684+
const full = resolve(dotFixtureDir, f);
685+
await mkdir(dirname(full), { recursive: true });
686+
await writeFile(full, '');
687+
}
688+
}
689+
await setupDotFixtures();
690+
691+
const dotExpectedWithDot = [
692+
'lib',
693+
'lib/.dotdir',
694+
'lib/.dotdir/regular.js',
695+
'lib/.dotdir/sub',
696+
'lib/.dotdir/sub/deep.js',
697+
'lib/.hidden.js',
698+
'lib/sub',
699+
'lib/sub/.hidden.js',
700+
'lib/sub/visible.js',
701+
'lib/visible.js',
702+
];
703+
const dotExpectedWithoutDot = [
704+
'lib',
705+
'lib/sub',
706+
'lib/sub/visible.js',
707+
'lib/visible.js',
708+
];
709+
const dotStarExpectedWithDot = [
710+
'lib/.hidden.js',
711+
'lib/visible.js',
712+
];
713+
const dotStarExpectedWithoutDot = [
714+
'lib/visible.js',
715+
];
716+
717+
describe('glob - dot', function() {
718+
const promisified = promisify(glob);
719+
720+
test('does not match dotfiles by default', async () => {
721+
const actual = (await promisified('lib/**', { cwd: dotFixtureDir })).sort();
722+
assert.deepStrictEqual(actual, dotExpectedWithoutDot);
723+
});
724+
725+
test('matches dotfiles and traverses dot-dirs when enabled', async () => {
726+
const actual = (await promisified('lib/**', {
727+
cwd: dotFixtureDir,
728+
dot: true,
729+
})).sort();
730+
assert.deepStrictEqual(actual, dotExpectedWithDot);
731+
});
732+
733+
test('respects dot option for STAR patterns', async () => {
734+
const actual = (await promisified('lib/*.js', {
735+
cwd: dotFixtureDir,
736+
dot: true,
737+
})).sort();
738+
assert.deepStrictEqual(actual, dotStarExpectedWithDot);
739+
});
740+
});
741+
742+
describe('globSync - dot', function() {
743+
test('does not match dotfiles by default', () => {
744+
const actual = globSync('lib/**', { cwd: dotFixtureDir }).sort();
745+
assert.deepStrictEqual(actual, dotExpectedWithoutDot);
746+
});
747+
748+
test('validates dot', () => {
749+
assert.throws(() => {
750+
globSync('lib/**', { cwd: dotFixtureDir, dot: 1 });
751+
}, {
752+
code: 'ERR_INVALID_ARG_TYPE',
753+
});
754+
});
755+
756+
test('matches dotfiles and traverses dot-dirs when enabled', () => {
757+
const actual = globSync('lib/**', {
758+
cwd: dotFixtureDir,
759+
dot: true,
760+
}).sort();
761+
assert.deepStrictEqual(actual, dotExpectedWithDot);
762+
});
763+
764+
test('respects dot option for STAR patterns', () => {
765+
const actual = globSync('lib/*.js', {
766+
cwd: dotFixtureDir,
767+
dot: true,
768+
}).sort();
769+
assert.deepStrictEqual(actual, dotStarExpectedWithDot);
770+
});
771+
772+
test('STAR patterns drop dotfiles by default', () => {
773+
const actual = globSync('lib/*.js', { cwd: dotFixtureDir }).sort();
774+
assert.deepStrictEqual(actual, dotStarExpectedWithoutDot);
775+
});
776+
});
777+
778+
describe('fsPromises glob - dot', function() {
779+
test('does not match dotfiles by default', async () => {
780+
const actual = [];
781+
for await (const item of asyncGlob('lib/**', { cwd: dotFixtureDir })) actual.push(item);
782+
actual.sort();
783+
assert.deepStrictEqual(actual, dotExpectedWithoutDot);
784+
});
785+
786+
test('matches dotfiles and traverses dot-dirs when enabled', async () => {
787+
const actual = [];
788+
for await (const item of asyncGlob('lib/**', {
789+
cwd: dotFixtureDir,
790+
dot: true,
791+
})) actual.push(item);
792+
actual.sort();
793+
assert.deepStrictEqual(actual, dotExpectedWithDot);
794+
});
795+
});

0 commit comments

Comments
 (0)