MartyO256/find-files-by-patterns

View on GitHub
src/files.ts

Summary

Maintainability
D
2 days
Test Coverage
import { realpath, realpathSync } from "fs";
import { resolve } from "path";
import { promisify } from "util";

import { upwardDirectories, upwardDirectoriesSync } from "./directories.js";
import { readdir, readdirs, readdirsSync, readdirSync } from "./readdirs.js";
import { isDirectory, isDirectorySync } from "./stat.js";

/**
 * A directory in the file system has a path and a depth relative to another
 * directory.
 */
interface Directory {
  /**
   * The path to this directory.
   */
  path: string;

  /**
   * The depth of this directory with respect to the traversal it is subject to.
   */
  depth: number;
}

/**
 * Constructs a directory at the start of a downward traversal.
 * @param path The path to the directory. In order to determine whether or not a
 * directory has been traversed, its real path should not be equal to any of the
 * traversed directory paths.
 * @returns A directory whose depth is `0` at the start of the downward
 * traversal.
 */
const startingDirectory = (path: string): Directory => ({ path, depth: 0 });

/**
 * Constructs a subdirectory given its path and its parent.
 * @param path The path to the subdirectory.
 * @param parent The parent directory.
 * @returns A directory at the given path whose depth is `1` greater than that
 * of its parent.
 */
const subdirectory = (path: string, parent: Directory): Directory => ({
  path,
  depth: parent.depth + 1,
});

/**
 * Constructs a function which determines whether or not a path has been
 * traversed.
 * @param paths The set of traversed paths.
 * @returns A function which determines whether or not a path has been
 * traversed.
 */
const pathHasNotBeenTraversed =
  (paths: Iterable<string>) =>
  (path: string): boolean => {
    for (const query of paths) {
      if (query === path) {
        return false;
      }
    }
    return true;
  };

const realpathNative = promisify(realpath.native);
const realpathNativeSync = realpathSync.native;

/**
 * Constructs an error for a negative maximum depth for downward file fetchers.
 * @param maximumDepth The input maximum depth.
 * @returns An error for a negative maximum depth.
 */
const negativeMaximumDepthError = (maximumDepth: number): Error =>
  new Error(`The maximum depth of ${maximumDepth} should be positive`);

/**
 * Handles the function overload of downward file fetchers.
 * @param startDirectory The first argument of the function.
 * @param maximumDepth The second argument of the function.
 * @throws If the maximum depth to return is negative.
 * @returns The validated arguments for the downward file fetchers function
 * call.
 */
export const handleDownwardFilesOverload = (
  startDirectory: number | string = ".",
  maximumDepth?: number,
): [string, number] => {
  if (typeof startDirectory === "number") {
    maximumDepth = startDirectory;
    startDirectory = ".";
  }
  if (maximumDepth < 0) {
    throw negativeMaximumDepthError(maximumDepth);
  }
  return [startDirectory, maximumDepth];
};

/**
 * Constructs an iterable over the downward files starting from a given path.
 * Symbolic links are followed, and the directories are traversed in
 * breadth-first order. Directories are read only once.
 * @param startDirectory The starting directory from which to start the downward
 * traversal.
 * @throws If the starting path is a file.
 * @throws If the starting path is inexistant.
 * @returns An iterable over the downward files.
 */
async function* unconstrainedDownwardFiles(
  startDirectory: string,
): AsyncIterable<string> {
  const start = resolve(startDirectory);
  const pendingFiles: Array<AsyncIterable<string>> = [readdir(start)];
  const traversedDirectories: string[] = [await realpathNative(start)];
  const isUntraversedDirectory = pathHasNotBeenTraversed(traversedDirectories);
  while (pendingFiles.length > 0) {
    for await (const file of pendingFiles.pop()) {
      yield file;
      if (await isDirectory(file)) {
        const realpath = await realpathNative(file);
        if (isUntraversedDirectory(realpath)) {
          traversedDirectories.push(realpath);
          pendingFiles.unshift(readdir(file));
        }
      }
    }
  }
}

/**
 * Constructs an iterable over the downward files starting from a given path and
 * down to a given maximum depth of a directory. Symbolic links are followed,
 * and the directories are traversed in breadth-first order. Directories are
 * read only once.
 * @param startDirectory The starting directory from which to start the downward
 * traversal.
 * @param maximumDepth The maximum depth of a read directory relative to the
 * start directory. This maximum depth should be zero or positive.
 * @throws If the starting path is a file.
 * @throws If the starting path is inexistant.
 * @throws If the maximum depth is negative.
 * @returns An iterable over the downward files down to the maximum depth.
 */
async function* constrainedDownwardFiles(
  startDirectory: string,
  maximumDepth: number,
): AsyncIterable<string> {
  const start = startingDirectory(resolve(startDirectory));
  const pendingDirectories: Directory[] = [start];
  const traversedDirectories: string[] = [await realpathNative(start.path)];
  const isUntraversed = pathHasNotBeenTraversed(traversedDirectories);
  while (pendingDirectories.length > 0) {
    const currentDirectory = pendingDirectories.pop();
    for await (const file of readdir(currentDirectory.path)) {
      yield file;
      if (await isDirectory(file)) {
        const directory = subdirectory(file, currentDirectory);
        const realpath = await realpathNative(file);
        if (directory.depth <= maximumDepth && isUntraversed(realpath)) {
          traversedDirectories.push(realpath);
          pendingDirectories.unshift(directory);
        }
      }
    }
  }
}

/**
 * Constructs an iterable over the downward files starting from the current
 * working directory. Symbolic links are followed, and the directories are
 * traversed in breadth-first order. Directories are read only once.
 * @returns An iterable over the downward files.
 */
export function downwardFiles(): AsyncIterable<string>;

/**
 * Constructs an iterable over the downward files starting from the current
 * working directory and down to a given maximum depth of a directory.
 * Symbolic links are followed, and the directories are traversed in
 * breadth-first order. Directories are read only once.
 * @param maximumDepth The maximum depth of a read directory relative to the
 * start directory. This maximum depth should be zero or positive.
 * @throws If the maximum depth is negative.
 * @returns An iterable over the downward files down to the maximum depth.
 */
export function downwardFiles(maximumDepth: number): AsyncIterable<string>;

/**
 * Constructs an iterable over the downward files starting from a given path.
 * Symbolic links are followed, and the directories are traversed in
 * breadth-first order. Directories are read only once.
 * @param startDirectory The starting directory from which to start the
 * downward traversal.
 * @throws If the starting path is a file.
 * @throws If the starting path is inexistant.
 * @returns An iterable over the downward files.
 */
export function downwardFiles(startDirectory: string): AsyncIterable<string>;

/**
 * Constructs an iterable over the downward files starting from a given path
 * and down to a given maximum depth of a directory. Symbolic links are
 * followed, and the directories are traversed in breadth-first order.
 * Directories are read only once.
 * @param startDirectory The starting directory from which to start the
 * downward traversal.
 * @param maximumDepth The maximum depth of a read directory relative to the
 * start directory. This maximum depth should be zero or positive.
 * @throws If the starting path is a file.
 * @throws If the starting path is inexistant.
 * @throws If the maximum depth is negative.
 * @returns An iterable over the downward files down to the maximum depth.
 */
export function downwardFiles(
  startDirectory: string,
  maximumDepth: number,
): AsyncIterable<string>;

/**
 * A downward files fetcher constructs an iterable over the files downwards from
 * a given directory path.
 */
export function downwardFiles(
  startDirectory?: number | string,
  maximumDepth?: number,
): AsyncIterable<string> {
  [startDirectory, maximumDepth] = handleDownwardFilesOverload(
    startDirectory,
    maximumDepth,
  );
  if (maximumDepth >= 0) {
    return constrainedDownwardFiles(startDirectory, maximumDepth);
  } else {
    return unconstrainedDownwardFiles(startDirectory);
  }
}

/**
 * Constructs an iterable over the downward files starting from a given path.
 * Symbolic links are followed, and the directories are traversed in
 * breadth-first order. Directories are read only once.
 * @param startDirectory The starting directory from which to start the downward
 * traversal.
 * @throws If the starting path is a file.
 * @throws If the starting path is inexistant.
 * @returns An iterable over the downward files.
 */
function* unconstrainedDownwardFilesSync(
  startDirectory: string,
): Iterable<string> {
  const start = resolve(startDirectory);
  const pendingFiles: Array<Iterable<string>> = [readdirSync(start)];
  const traversedDirectories: string[] = [realpathNativeSync(start)];
  const isUntraversedDirectory = pathHasNotBeenTraversed(traversedDirectories);
  while (pendingFiles.length > 0) {
    for (const file of pendingFiles.pop()) {
      yield file;
      if (isDirectorySync(file)) {
        const realpath = realpathNativeSync(file);
        if (isUntraversedDirectory(realpath)) {
          traversedDirectories.push(realpath);
          pendingFiles.unshift(readdirSync(file));
        }
      }
    }
  }
}

/**
 * Constructs an iterable over the downward files starting from a given path and
 * down to a given maximum depth of a directory. Symbolic links are followed,
 * and the directories are traversed in breadth-first order. Directories are
 * read only once.
 * @param startDirectory The starting directory from which to start the downward
 * traversal.
 * @param maximumDepth The maximum depth of a read directory relative to the
 * start directory. This maximum depth should be zero or positive.
 * @throws If the starting path is a file.
 * @throws If the starting path is inexistant.
 * @throws If the maximum depth is negative.
 * @returns An iterable over the downward files down to the maximum depth.
 */
function* constrainedDownwardFilesSync(
  startDirectory: string,
  maximumDepth: number,
): Iterable<string> {
  const start = startingDirectory(resolve(startDirectory));
  const pendingDirectories: Directory[] = [start];
  const traversedDirectories: string[] = [realpathNativeSync(start.path)];
  const isUntraversed = pathHasNotBeenTraversed(traversedDirectories);
  while (pendingDirectories.length > 0) {
    const currentDirectory = pendingDirectories.pop();
    for (const file of readdirSync(currentDirectory.path)) {
      yield file;
      if (isDirectorySync(file)) {
        const directory = subdirectory(file, currentDirectory);
        const realpath = realpathNativeSync(file);
        if (directory.depth <= maximumDepth && isUntraversed(realpath)) {
          traversedDirectories.push(realpath);
          pendingDirectories.unshift(directory);
        }
      }
    }
  }
}

/**
 * Constructs an iterable over the downward files starting from the current
 * working directory. Symbolic links are followed, and the directories are
 * traversed in breadth-first order. Directories are read only once.
 * @returns An iterable over the downward files.
 */
export function downwardFilesSync(): Iterable<string>;

/**
 * Constructs an iterable over the downward files starting from the current
 * working directory and down to a given maximum depth of a directory.
 * Symbolic links are followed, and the directories are traversed in
 * breadth-first order. Directories are read only once.
 * @param maximumDepth The maximum depth of a read directory relative to the
 * start directory. This maximum depth should be zero or positive.
 * @throws If the maximum depth is negative.
 * @returns An iterable over the downward files down to the maximum depth.
 */
export function downwardFilesSync(maximumDepth: number): Iterable<string>;

/**
 * Constructs an iterable over the downward files starting from a given path.
 * Symbolic links are followed, and the directories are traversed in
 * breadth-first order. Directories are read only once.
 * @param startDirectory The starting directory from which to start the
 * downward traversal.
 * @throws If the starting path is a file.
 * @throws If the starting path is inexistant.
 * @returns An iterable over the downward files.
 */
export function downwardFilesSync(startDirectory: string): Iterable<string>;

/**
 * Constructs an iterable over the downward files starting from a given path
 * and down to a given maximum depth of a directory. Symbolic links are
 * followed, and the directories are traversed in breadth-first order.
 * Directories are read only once.
 * @param startDirectory The starting directory from which to start the
 * downward traversal.
 * @param maximumDepth The maximum depth of a read directory relative to the
 * start directory. This maximum depth should be zero or positive.
 * @throws If the starting path is a file.
 * @throws If the starting path is inexistant.
 * @throws If the maximum depth is negative.
 * @returns An iterable over the downward files down to the maximum depth.
 */
export function downwardFilesSync(
  startDirectory: string,
  maximumDepth: number,
): Iterable<string>;

/**
 * A downward files fetcher constructs an iterable over the files downwards from
 * a given directory path.
 */
export function downwardFilesSync(
  startDirectory?: number | string,
  maximumDepth?: number,
): Iterable<string> {
  [startDirectory, maximumDepth] = handleDownwardFilesOverload(
    startDirectory,
    maximumDepth,
  );
  if (maximumDepth >= 0) {
    return constrainedDownwardFilesSync(startDirectory, maximumDepth);
  } else {
    return unconstrainedDownwardFilesSync(startDirectory);
  }
}

/**
 * Handles the function overload of upward file fetchers.
 * @param startPath The first argument of the function.
 * @param maximumHeight The second argument of the function.
 * @returns The validated arguments for the upward file fetchers function call.
 */
const handleUpwardFilesOverload = (
  startPath: number | string = ".",
  maximumHeight?: number | string,
): [string, undefined | number | string] => {
  if (typeof startPath === "number") {
    return [".", startPath];
  }
  return [startPath, maximumHeight];
};

/**
 * Constructs an iterable over the files in the upward directories relative to
 * the current working directory, up to the root inclusively of the current
 * working directory. The directories are traversed in increasing order of
 * height relative to the start directory. The start directory is not read.
 * @returns An iterable over the upward files.
 */
export function upwardFiles(): AsyncIterable<string>;

/**
 * Constructs an iterable over the files in the upward directories relative to
 * the current working directory, up to the upward directory whose height
 * relative to the current working directory is equal to the given maximum
 * height. The directories are traversed in increasing order of height
 * relative to the start directory. The start directory is not read.
 * @param maximumHeight The maximum height of a directory. The height of the
 * start directory is zero. This value should be greater than or equal to one.
 * @returns An iterable over the upward files.
 */
export function upwardFiles(maximumHeight: number): AsyncIterable<string>;

/**
 * Constructs an iterable over the files in the upward directories relative to
 * the start path, up to the root inclusively of the start path directory. The
 * directories are traversed in increasing order of height relative to the
 * start directory. The start path is not read if it is a directory. If the
 * start path is a file, then its directory is the first directory to be read.
 * @param startPath The start path of the upward traversal.
 * @returns An iterable over the upward files.
 */
export function upwardFiles(startPath: string): AsyncIterable<string>;

/**
 * Constructs an iterable over the files in the upward directories relative to
 * the start path, up to the upward directory whose height relative to the
 * start path is equal to the given maximum height. The directories are
 * traversed in increasing order of height relative to the start directory.
 * The start path is not read if it is a directory. If the start path is a
 * file, then its directory is the first directory to be read, if its height
 * does not exceed the maximum height.
 * @param startPath The start path of the upward traversal.
 * @param maximumHeight The maximum height of a directory. The height of the
 * start path is zero. This value should be greater than or equal to one.
 * @returns An iterable over the upward files.
 */
export function upwardFiles(
  startPath: string,
  maximumHeight: number,
): AsyncIterable<string>;

/**
 * Constructs an iterable over the files in the upward directories relative to
 * the start path, up to an end path. The directories are traversed in
 * increasing order of height relative to the start directory. The start path
 * is not read if it is a directory. If the start path is a file, then its
 * directory is the first directory to be read. If the end directory is not
 * parent to the start path, then all the upward paths from the start path are
 * yielded up to its root. The iteration stops once the end directory is
 * reached and its files are yielded.
 * @param startPath The start path of the upward traversal.
 * @param endDirectory The end directory at which point all the upward
 * directories have been read.
 * @returns An iterable over the upward files.
 */
export function upwardFiles(
  startPath: string,
  endDirectory: string,
): AsyncIterable<string>;

/**
 * An upward file fetcher constructs an iterable over the files in upward
 * directories relative to a start path.
 */
export function upwardFiles(
  startPath?: number | string,
  upperBound?: number | string,
): AsyncIterable<string> {
  [startPath, upperBound] = handleUpwardFilesOverload(startPath, upperBound);
  return readdirs(
    (
      upwardDirectories as (
        startPath?: string,
        upperBound?: number | string,
      ) => AsyncIterable<string>
    )(startPath, upperBound),
  );
}

/**
 * Constructs an iterable over the files in the upward directories relative to
 * the current working directory, up to the root inclusively of the current
 * working directory. The directories are traversed in increasing order of
 * height relative to the start directory. The start directory is not read.
 * @returns An iterable over the upward files.
 */
export function upwardFilesSync(): Iterable<string>;

/**
 * Constructs an iterable over the files in the upward directories relative to
 * the current working directory, up to the upward directory whose height
 * relative to the current working directory is equal to the given maximum
 * height. The directories are traversed in increasing order of height
 * relative to the start directory. The start directory is not read.
 * @param maximumHeight The maximum height of a directory. The height of the
 * start directory is zero. This value should be greater than or equal to one.
 * @returns An iterable over the upward files.
 */
export function upwardFilesSync(maximumHeight: number): Iterable<string>;

/**
 * Constructs an iterable over the files in the upward directories relative to
 * the start path, up to the root inclusively of the start path directory. The
 * directories are traversed in increasing order of height relative to the
 * start directory. The start path is not read if it is a directory. If the
 * start path is a file, then its directory is the first directory to be read.
 * @param startPath The start path of the upward traversal.
 * @returns An iterable over the upward files.
 */
export function upwardFilesSync(startPath: string): Iterable<string>;

/**
 * Constructs an iterable over the files in the upward directories relative to
 * the start path, up to the upward directory whose height relative to the
 * start path is equal to the given maximum height. The directories are
 * traversed in increasing order of height relative to the start directory.
 * The start path is not read if it is a directory. If the start path is a
 * file, then its directory is the first directory to be read, if its height
 * does not exceed the maximum height.
 * @param startPath The start path of the upward traversal.
 * @param maximumHeight The maximum height of a directory. The height of the
 * start path is zero. This value should be greater than or equal to one.
 * @returns An iterable over the upward files.
 */
export function upwardFilesSync(
  startPath: string,
  maximumHeight: number,
): Iterable<string>;

/**
 * Constructs an iterable over the files in the upward directories relative to
 * the start path, up to an end path. The directories are traversed in
 * increasing order of height relative to the start directory. The start path
 * is not read if it is a directory. If the start path is a file, then its
 * directory is the first directory to be read. If the end directory is not
 * parent to the start path, then all the upward paths from the start path are
 * yielded up to its root. The iteration stops once the end directory is
 * reached and its files are yielded.
 * @param startPath The start path of the upward traversal.
 * @param endDirectory The end directory at which point all the upward
 * directories have been read.
 * @returns An iterable over the upward files.
 */
export function upwardFilesSync(
  startPath: string,
  endDirectory: string,
): Iterable<string>;

/**
 * An upward file fetcher constructs an iterable over the files in upward
 * directories relative to a start path.
 */
export function upwardFilesSync(
  startPath?: number | string,
  upperBound?: number | string,
): Iterable<string> {
  [startPath, upperBound] = handleUpwardFilesOverload(startPath, upperBound);
  return readdirsSync(
    (
      upwardDirectoriesSync as (
        startPath?: string,
        upperBound?: number | string,
      ) => Iterable<string>
    )(startPath, upperBound),
  );
}