summaryrefslogtreecommitdiff
path: root/std/fs/expand_glob.ts
diff options
context:
space:
mode:
Diffstat (limited to 'std/fs/expand_glob.ts')
-rw-r--r--std/fs/expand_glob.ts251
1 files changed, 251 insertions, 0 deletions
diff --git a/std/fs/expand_glob.ts b/std/fs/expand_glob.ts
new file mode 100644
index 000000000..c43e09d40
--- /dev/null
+++ b/std/fs/expand_glob.ts
@@ -0,0 +1,251 @@
+import {
+ GlobOptions,
+ SEP_PATTERN,
+ globToRegExp,
+ isAbsolute,
+ isGlob,
+ isWindows,
+ joinGlobs,
+ normalize
+} from "../path/mod.ts";
+import { WalkInfo, walk, walkSync } from "./walk.ts";
+const { cwd, stat, statSync } = Deno;
+type FileInfo = Deno.FileInfo;
+
+export interface ExpandGlobOptions extends GlobOptions {
+ root?: string;
+ exclude?: string[];
+ includeDirs?: boolean;
+}
+
+interface SplitPath {
+ segments: string[];
+ isAbsolute: boolean;
+ hasTrailingSep: boolean;
+ // Defined for any absolute Windows path.
+ winRoot?: string;
+}
+
+// TODO: Maybe make this public somewhere.
+function split(path: string): SplitPath {
+ const s = SEP_PATTERN.source;
+ const segments = path
+ .replace(new RegExp(`^${s}|${s}$`, "g"), "")
+ .split(SEP_PATTERN);
+ const isAbsolute_ = isAbsolute(path);
+ return {
+ segments,
+ isAbsolute: isAbsolute_,
+ hasTrailingSep: !!path.match(new RegExp(`${s}$`)),
+ winRoot: isWindows && isAbsolute_ ? segments.shift() : undefined
+ };
+}
+
+/**
+ * Expand the glob string from the specified `root` directory and yield each
+ * result as a `WalkInfo` object.
+ */
+// TODO: Use a proper glob expansion algorithm.
+// This is a very incomplete solution. The whole directory tree from `root` is
+// walked and parent paths are not supported.
+export async function* expandGlob(
+ glob: string,
+ {
+ root = cwd(),
+ exclude = [],
+ includeDirs = true,
+ extended = false,
+ globstar = false
+ }: ExpandGlobOptions = {}
+): AsyncIterableIterator<WalkInfo> {
+ const globOptions: GlobOptions = { extended, globstar };
+ const absRoot = isAbsolute(root)
+ ? normalize(root)
+ : joinGlobs([cwd(), root], globOptions);
+ const resolveFromRoot = (path: string): string =>
+ isAbsolute(path)
+ ? normalize(path)
+ : joinGlobs([absRoot, path], globOptions);
+ const excludePatterns = exclude
+ .map(resolveFromRoot)
+ .map((s: string): RegExp => globToRegExp(s, globOptions));
+ const shouldInclude = ({ filename }: WalkInfo): boolean =>
+ !excludePatterns.some((p: RegExp): boolean => !!filename.match(p));
+ const { segments, hasTrailingSep, winRoot } = split(resolveFromRoot(glob));
+
+ let fixedRoot = winRoot != undefined ? winRoot : "/";
+ while (segments.length > 0 && !isGlob(segments[0])) {
+ fixedRoot = joinGlobs([fixedRoot, segments.shift()!], globOptions);
+ }
+
+ let fixedRootInfo: WalkInfo;
+ try {
+ fixedRootInfo = { filename: fixedRoot, info: await stat(fixedRoot) };
+ } catch {
+ return;
+ }
+
+ async function* advanceMatch(
+ walkInfo: WalkInfo,
+ globSegment: string
+ ): AsyncIterableIterator<WalkInfo> {
+ if (!walkInfo.info.isDirectory()) {
+ return;
+ } else if (globSegment == "..") {
+ const parentPath = joinGlobs([walkInfo.filename, ".."], globOptions);
+ try {
+ return yield* [
+ { filename: parentPath, info: await stat(parentPath) }
+ ].filter(shouldInclude);
+ } catch {
+ return;
+ }
+ } else if (globSegment == "**") {
+ return yield* walk(walkInfo.filename, {
+ includeFiles: false,
+ skip: excludePatterns
+ });
+ }
+ yield* walk(walkInfo.filename, {
+ maxDepth: 1,
+ match: [
+ globToRegExp(
+ joinGlobs([walkInfo.filename, globSegment], globOptions),
+ globOptions
+ )
+ ],
+ skip: excludePatterns
+ });
+ }
+
+ let currentMatches: WalkInfo[] = [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, FileInfo> = new Map();
+ for (const currentMatch of currentMatches) {
+ for await (const nextMatch of advanceMatch(currentMatch, segment)) {
+ nextMatchMap.set(nextMatch.filename, nextMatch.info);
+ }
+ }
+ currentMatches = [...nextMatchMap].sort().map(
+ ([filename, info]): WalkInfo => ({
+ filename,
+ info
+ })
+ );
+ }
+ if (hasTrailingSep) {
+ currentMatches = currentMatches.filter(
+ ({ info }): boolean => info.isDirectory()
+ );
+ }
+ if (!includeDirs) {
+ currentMatches = currentMatches.filter(
+ ({ info }): boolean => !info.isDirectory()
+ );
+ }
+ yield* currentMatches;
+}
+
+/** Synchronous version of `expandGlob()`. */
+// TODO: As `expandGlob()`.
+export function* expandGlobSync(
+ glob: string,
+ {
+ root = cwd(),
+ exclude = [],
+ includeDirs = true,
+ extended = false,
+ globstar = false
+ }: ExpandGlobOptions = {}
+): IterableIterator<WalkInfo> {
+ const globOptions: GlobOptions = { extended, globstar };
+ const absRoot = isAbsolute(root)
+ ? normalize(root)
+ : joinGlobs([cwd(), root], globOptions);
+ const resolveFromRoot = (path: string): string =>
+ isAbsolute(path)
+ ? normalize(path)
+ : joinGlobs([absRoot, path], globOptions);
+ const excludePatterns = exclude
+ .map(resolveFromRoot)
+ .map((s: string): RegExp => globToRegExp(s, globOptions));
+ const shouldInclude = ({ filename }: WalkInfo): boolean =>
+ !excludePatterns.some((p: RegExp): boolean => !!filename.match(p));
+ const { segments, hasTrailingSep, winRoot } = split(resolveFromRoot(glob));
+
+ let fixedRoot = winRoot != undefined ? winRoot : "/";
+ while (segments.length > 0 && !isGlob(segments[0])) {
+ fixedRoot = joinGlobs([fixedRoot, segments.shift()!], globOptions);
+ }
+
+ let fixedRootInfo: WalkInfo;
+ try {
+ fixedRootInfo = { filename: fixedRoot, info: statSync(fixedRoot) };
+ } catch {
+ return;
+ }
+
+ function* advanceMatch(
+ walkInfo: WalkInfo,
+ globSegment: string
+ ): IterableIterator<WalkInfo> {
+ if (!walkInfo.info.isDirectory()) {
+ return;
+ } else if (globSegment == "..") {
+ const parentPath = joinGlobs([walkInfo.filename, ".."], globOptions);
+ try {
+ return yield* [
+ { filename: parentPath, info: statSync(parentPath) }
+ ].filter(shouldInclude);
+ } catch {
+ return;
+ }
+ } else if (globSegment == "**") {
+ return yield* walkSync(walkInfo.filename, {
+ includeFiles: false,
+ skip: excludePatterns
+ });
+ }
+ yield* walkSync(walkInfo.filename, {
+ maxDepth: 1,
+ match: [
+ globToRegExp(
+ joinGlobs([walkInfo.filename, globSegment], globOptions),
+ globOptions
+ )
+ ],
+ skip: excludePatterns
+ });
+ }
+
+ let currentMatches: WalkInfo[] = [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, FileInfo> = new Map();
+ for (const currentMatch of currentMatches) {
+ for (const nextMatch of advanceMatch(currentMatch, segment)) {
+ nextMatchMap.set(nextMatch.filename, nextMatch.info);
+ }
+ }
+ currentMatches = [...nextMatchMap].sort().map(
+ ([filename, info]): WalkInfo => ({
+ filename,
+ info
+ })
+ );
+ }
+ if (hasTrailingSep) {
+ currentMatches = currentMatches.filter(
+ ({ info }): boolean => info.isDirectory()
+ );
+ }
+ if (!includeDirs) {
+ currentMatches = currentMatches.filter(
+ ({ info }): boolean => !info.isDirectory()
+ );
+ }
+ yield* currentMatches;
+}