Latest
The Standard Library has been moved to JSR. See the blog post for details.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.import { type GlobOptions, globToRegExp } from "../path/glob_to_regexp.ts";import { joinGlobs } from "../path/join_globs.ts";import { isGlob } from "../path/is_glob.ts";import { isAbsolute } from "../path/is_absolute.ts";import { resolve } from "../path/resolve.ts";import { SEPARATOR_PATTERN } from "../path/constants.ts";import { walk, walkSync } from "./walk.ts";import { assert } from "../assert/assert.ts";import { toPathString } from "./_to_path_string.ts";import { createWalkEntry, createWalkEntrySync, type WalkEntry,} from "./_create_walk_entry.ts";
export type { GlobOptions, WalkEntry };
const isWindows = Deno.build.os === "windows";
/** Options for {@linkcode expandGlob} and {@linkcode expandGlobSync}. */export interface ExpandGlobOptions extends Omit<GlobOptions, "os"> { /** File path where to expand from. */ root?: string; /** List of glob patterns to be excluded from the expansion. */ exclude?: string[]; /** * Whether to include directories in entries. * * @default {true} */ includeDirs?: boolean; /** * Whether to follow symbolic links. * * @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;}
interface SplitPath { segments: string[]; isAbsolute: boolean; hasTrailingSep: boolean; // Defined for any absolute Windows path. winRoot?: string;}
function split(path: string): SplitPath { const s = SEPARATOR_PATTERN.source; const segments = path .replace(new RegExp(`^${s}|${s}$`, "g"), "") .split(SEPARATOR_PATTERN); const isAbsolute_ = isAbsolute(path); return { segments, isAbsolute: isAbsolute_, hasTrailingSep: !!path.match(new RegExp(`${s}$`)), winRoot: isWindows && isAbsolute_ ? segments.shift() : undefined, };}
function throwUnlessNotFound(error: unknown) { if (!(error instanceof Deno.errors.NotFound)) { throw error; }}
function comparePath(a: WalkEntry, b: WalkEntry): number { if (a.path < b.path) return -1; if (a.path > b.path) return 1; return 0;}
/** * Returns an async iterator that yields each file path matching the given glob * pattern. The file paths are relative to the provided `root` directory. * If `root` is not provided, the current working directory is used. * The `root` directory is not included in the yielded file paths. * * Requires the `--allow-read` flag. * * @param glob The glob pattern to expand. * @param options Additional options for the expansion. * @returns An async iterator that yields each walk entry matching the glob * pattern. * * @example Basic usage * * File structure: * ``` * folder * ├── script.ts * └── foo.ts * ``` * * ```ts * // script.ts * import { expandGlob } from "https://deno.land/std@$STD_VERSION/fs/expand_glob.ts"; * * const entries = []; * for await (const entry of expandGlob("*.ts")) { * entries.push(entry); * } * * entries[0]!.path; // "/Users/user/folder/script.ts" * entries[0]!.name; // "script.ts" * entries[0]!.isFile; // false * entries[0]!.isDirectory; // true * entries[0]!.isSymlink; // false * * entries[1]!.path; // "/Users/user/folder/foo.ts" * entries[1]!.name; // "foo.ts" * entries[1]!.isFile; // true * entries[1]!.isDirectory; // false * entries[1]!.isSymlink; // false * ``` */export async function* expandGlob( glob: string | URL, { root, exclude = [], includeDirs = true, extended = true, globstar = true, caseInsensitive, followSymlinks, canonicalize, }: ExpandGlobOptions = {},): AsyncIterableIterator<WalkEntry> { const { segments, isAbsolute: isGlobAbsolute, hasTrailingSep, winRoot, } = split(toPathString(glob)); root ??= isGlobAbsolute ? winRoot ?? "/" : Deno.cwd();
const globOptions: GlobOptions = { extended, globstar, caseInsensitive }; const absRoot = isGlobAbsolute ? root : resolve(root!); // root is always string here const resolveFromRoot = (path: string): string => resolve(absRoot, path); const excludePatterns = exclude .map(resolveFromRoot) .map((s: string): RegExp => globToRegExp(s, globOptions)); const shouldInclude = (path: string): boolean => !excludePatterns.some((p: RegExp): boolean => !!path.match(p));
let fixedRoot = isGlobAbsolute ? winRoot !== undefined ? winRoot : "/" : absRoot; while (segments.length > 0 && !isGlob(segments[0]!)) { const seg = segments.shift(); assert(seg !== undefined); fixedRoot = joinGlobs([fixedRoot, seg], globOptions); }
let fixedRootInfo: WalkEntry; try { fixedRootInfo = await createWalkEntry(fixedRoot); } catch (error) { return throwUnlessNotFound(error); }
async function* advanceMatch( walkInfo: WalkEntry, globSegment: string, ): AsyncIterableIterator<WalkEntry> { if (!walkInfo.isDirectory) { return; } else if (globSegment === "..") { const parentPath = joinGlobs([walkInfo.path, ".."], globOptions); try { if (shouldInclude(parentPath)) { return yield await createWalkEntry(parentPath); } } catch (error) { throwUnlessNotFound(error); } return; } else if (globSegment === "**") { return yield* walk(walkInfo.path, { skip: excludePatterns, maxDepth: globstar ? Infinity : 1, followSymlinks, canonicalize, }); } const globPattern = globToRegExp(globSegment, globOptions); for await ( const walkEntry of walk(walkInfo.path, { maxDepth: 1, skip: excludePatterns, followSymlinks, }) ) { if ( walkEntry.path !== walkInfo.path && walkEntry.name.match(globPattern) ) { yield walkEntry; } } }
let currentMatches: WalkEntry[] = [fixedRootInfo]; for (const segment of segments) { // Advancing the list of current matches may introduce duplicates, so we // pass everything through this Map. const nextMatchMap: Map<string, WalkEntry> = new Map(); await Promise.all( currentMatches.map(async (currentMatch) => { for await (const nextMatch of advanceMatch(currentMatch, segment)) { nextMatchMap.set(nextMatch.path, nextMatch); } }), ); currentMatches = [...nextMatchMap.values()].sort(comparePath); }
if (hasTrailingSep) { currentMatches = currentMatches.filter( (entry: WalkEntry): boolean => entry.isDirectory, ); } if (!includeDirs) { currentMatches = currentMatches.filter( (entry: WalkEntry): boolean => !entry.isDirectory, ); } yield* currentMatches;}
/** * Returns an iterator that yields each file path matching the given glob * pattern. The file paths are relative to the provided `root` directory. * If `root` is not provided, the current working directory is used. * The `root` directory is not included in the yielded file paths. * * Requires the `--allow-read` flag. * * @param glob The glob pattern to expand. * @param options Additional options for the expansion. * @returns An iterator that yields each walk entry matching the glob pattern. * * @example Basic usage * * File structure: * ``` * folder * ├── script.ts * └── foo.ts * ``` * * ```ts * // script.ts * import { expandGlobSync } from "https://deno.land/std@$STD_VERSION/fs/expand_glob.ts"; * * const entries = []; * for (const entry of expandGlobSync("*.ts")) { * entries.push(entry); * } * * entries[0]!.path; // "/Users/user/folder/script.ts" * entries[0]!.name; // "script.ts" * entries[0]!.isFile; // false * entries[0]!.isDirectory; // true * entries[0]!.isSymlink; // false * * entries[1]!.path; // "/Users/user/folder/foo.ts" * entries[1]!.name; // "foo.ts" * entries[1]!.isFile; // true * entries[1]!.isDirectory; // false * entries[1]!.isSymlink; // false * ``` */export function* expandGlobSync( glob: string | URL, { root, exclude = [], includeDirs = true, extended = true, globstar = true, caseInsensitive, followSymlinks, canonicalize, }: ExpandGlobOptions = {},): IterableIterator<WalkEntry> { const { segments, isAbsolute: isGlobAbsolute, hasTrailingSep, winRoot, } = split(toPathString(glob)); root ??= isGlobAbsolute ? winRoot ?? "/" : Deno.cwd();
const globOptions: GlobOptions = { extended, globstar, caseInsensitive }; const absRoot = isGlobAbsolute ? root : resolve(root!); // root is always string here const resolveFromRoot = (path: string): string => resolve(absRoot, path); const excludePatterns = exclude .map(resolveFromRoot) .map((s: string): RegExp => globToRegExp(s, globOptions)); const shouldInclude = (path: string): boolean => !excludePatterns.some((p: RegExp): boolean => !!path.match(p));
let fixedRoot = isGlobAbsolute ? winRoot !== undefined ? winRoot : "/" : absRoot; while (segments.length > 0 && !isGlob(segments[0]!)) { const seg = segments.shift(); assert(seg !== undefined); fixedRoot = joinGlobs([fixedRoot, seg], globOptions); }
let fixedRootInfo: WalkEntry; try { fixedRootInfo = createWalkEntrySync(fixedRoot); } catch (error) { return throwUnlessNotFound(error); }
function* advanceMatch( walkInfo: WalkEntry, globSegment: string, ): IterableIterator<WalkEntry> { if (!walkInfo.isDirectory) { return; } else if (globSegment === "..") { const parentPath = joinGlobs([walkInfo.path, ".."], globOptions); try { if (shouldInclude(parentPath)) { return yield createWalkEntrySync(parentPath); } } catch (error) { throwUnlessNotFound(error); } return; } else if (globSegment === "**") { return yield* walkSync(walkInfo.path, { skip: excludePatterns, maxDepth: globstar ? Infinity : 1, followSymlinks, canonicalize, }); } const globPattern = globToRegExp(globSegment, globOptions); for ( const walkEntry of walkSync(walkInfo.path, { maxDepth: 1, skip: excludePatterns, followSymlinks, }) ) { if ( walkEntry.path !== walkInfo.path && walkEntry.name.match(globPattern) ) { yield walkEntry; } } }
let currentMatches: WalkEntry[] = [fixedRootInfo]; for (const segment of segments) { // Advancing the list of current matches may introduce duplicates, so we // pass everything through this Map. const nextMatchMap: Map<string, WalkEntry> = new Map(); for (const currentMatch of currentMatches) { for (const nextMatch of advanceMatch(currentMatch, segment)) { nextMatchMap.set(nextMatch.path, nextMatch); } } currentMatches = [...nextMatchMap.values()].sort(comparePath); }
if (hasTrailingSep) { currentMatches = currentMatches.filter( (entry: WalkEntry): boolean => entry.isDirectory, ); } if (!includeDirs) { currentMatches = currentMatches.filter( (entry: WalkEntry): boolean => !entry.isDirectory, ); } yield* currentMatches;}