diff --git a/fs/expand_glob.ts b/fs/expand_glob.ts index 68ddd351a..3bc2dae48 100644 --- a/fs/expand_glob.ts +++ b/fs/expand_glob.ts @@ -19,6 +19,12 @@ export interface ExpandGlobOptions extends Omit { exclude?: string[]; includeDirs?: boolean; followSymlinks?: boolean; + /** + * Indicates whether the followed symlink's path should be canonicalized. + * This option works only if `followSymlinks` is not `false`. + * @default {true} + */ + canonicalize?: boolean; } interface SplitPath { @@ -80,6 +86,7 @@ export async function* expandGlob( globstar = true, caseInsensitive, followSymlinks, + canonicalize, }: ExpandGlobOptions = {}, ): AsyncIterableIterator { const globOptions: GlobOptions = { extended, globstar, caseInsensitive }; @@ -134,6 +141,7 @@ export async function* expandGlob( skip: excludePatterns, maxDepth: globstar ? Infinity : 1, followSymlinks, + canonicalize, }); } const globPattern = globToRegExp(globSegment, globOptions); @@ -202,6 +210,7 @@ export function* expandGlobSync( globstar = true, caseInsensitive, followSymlinks, + canonicalize, }: ExpandGlobOptions = {}, ): IterableIterator { const globOptions: GlobOptions = { extended, globstar, caseInsensitive }; @@ -256,6 +265,7 @@ export function* expandGlobSync( skip: excludePatterns, maxDepth: globstar ? Infinity : 1, followSymlinks, + canonicalize, }); } const globPattern = globToRegExp(globSegment, globOptions); diff --git a/fs/expand_glob_test.ts b/fs/expand_glob_test.ts index 4a2970ffc..27bdc0dfe 100644 --- a/fs/expand_glob_test.ts +++ b/fs/expand_glob_test.ts @@ -156,3 +156,28 @@ Deno.test("expandGlobFollowSymlink", async function () { }; assertEquals(await expandGlobArray("*", options), ["abc"]); }); + +Deno.test("expandGlobFollowSymlink with canonicalize", async function () { + const options = { + ...EG_OPTIONS, + root: join(EG_OPTIONS.root!, "."), + followSymlinks: true, + }; + assertEquals( + await expandGlobArray("**/abc", options), + ["abc", join("subdir", "abc")], + ); +}); + +Deno.test("expandGlobFollowSymlink without canonicalize", async function () { + const options = { + ...EG_OPTIONS, + root: join(EG_OPTIONS.root!, "."), + followSymlinks: true, + canonicalize: false, + }; + assertEquals( + await expandGlobArray("**/abc", options), + ["abc", join("link", "abc"), join("subdir", "abc")], + ); +}); diff --git a/fs/testdata/walk/symlink/a/z b/fs/testdata/walk/symlink/a/z new file mode 100644 index 000000000..e69de29bb diff --git a/fs/testdata/walk/symlink/b b/fs/testdata/walk/symlink/b new file mode 120000 index 000000000..2e65efe2a --- /dev/null +++ b/fs/testdata/walk/symlink/b @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/fs/walk.ts b/fs/walk.ts index 383a46018..7d99afd9c 100644 --- a/fs/walk.ts +++ b/fs/walk.ts @@ -75,6 +75,12 @@ export interface WalkOptions { * @default {false} */ followSymlinks?: boolean; + /** + * Indicates whether the followed symlink's path should be canonicalized. + * This option works only if `followSymlinks` is not `false`. + * @default {true} + */ + canonicalize?: boolean; /** * List of file extensions used to filter entries. * If specified, entries without the file extension specified by this option are excluded. @@ -119,6 +125,7 @@ export async function* walk( includeDirs = true, includeSymlinks = true, followSymlinks = false, + canonicalize = true, exts = undefined, match = undefined, skip = undefined, @@ -147,11 +154,14 @@ export async function* walk( } continue; } - path = await Deno.realPath(path); + const realPath = await Deno.realPath(path); + if (canonicalize) { + path = realPath; + } // Caveat emptor: don't assume |path| is not a symlink. realpath() // resolves symlinks but another process can replace the file system // entity with a different type of entity before we call lstat(). - ({ isSymlink, isDirectory } = await Deno.lstat(path)); + ({ isSymlink, isDirectory } = await Deno.lstat(realPath)); } if (isSymlink || isDirectory) { @@ -183,6 +193,7 @@ export function* walkSync( includeDirs = true, includeSymlinks = true, followSymlinks = false, + canonicalize = true, exts = undefined, match = undefined, skip = undefined, @@ -216,11 +227,14 @@ export function* walkSync( } continue; } - path = Deno.realPathSync(path); + const realPath = Deno.realPathSync(path); + if (canonicalize) { + path = realPath; + } // Caveat emptor: don't assume |path| is not a symlink. realpath() // resolves symlinks but another process can replace the file system // entity with a different type of entity before we call lstat(). - ({ isSymlink, isDirectory } = Deno.lstatSync(path)); + ({ isSymlink, isDirectory } = Deno.lstatSync(realPath)); } if (isSymlink || isDirectory) { diff --git a/fs/walk_test.ts b/fs/walk_test.ts index c5ba8a4bf..dc316c0af 100644 --- a/fs/walk_test.ts +++ b/fs/walk_test.ts @@ -74,12 +74,18 @@ Deno.test("[fs/walk] skip", async () => // https://github.com/denoland/deno_std/issues/1358 Deno.test("[fs/walk] symlink", async () => - await assertWalkPaths("symlink", [".", "x", "x"], { + await assertWalkPaths("symlink", [".", "a", "a/z", "a", "a/z", "x", "x"], { followSymlinks: true, })); +Deno.test("[fs/walk] symlink without canonicalize", async () => + await assertWalkPaths("symlink", [".", "a", "a/z", "b", "b/z", "x", "y"], { + followSymlinks: true, + canonicalize: false, + })); + Deno.test("[fs/walk] symlink without followSymlink", async () => { - await assertWalkPaths("symlink", [".", "x", "y"], { + await assertWalkPaths("symlink", [".", "a", "a/z", "b", "x", "y"], { followSymlinks: false, }); });