listFiles static method

List<String> listFiles({
  1. String beneath = '',
  2. required Iterable<String> listDir(
    1. String
    ),
  3. required Ignore? ignoreForDir(
    1. String
    ),
  4. required bool isDir(
    1. String
    ),
  5. bool includeDirs = false,
})

Returns all the files in the tree under (and including) beneath not ignored by ignore-files from root and down.

Represents paths normalized using '/' as directory separator. The empty relative path is '.', no '..' are allowed.

beneath must start with root and even if it is a directory it should not end with '/', if beneath is not provided, everything under root is included.

listDir should enumerate the immediate contents of a given directory, returning paths including root.

isDir should return true if the argument is a directory. It will only be queried with file-names under (and including) beneath

ignoreForDir should retrieve the ignore rules for a single directory or return null if there is no ignore rules.

If includeDirs is true non-ignored directories will be included in the result (including beneath).

This example program lists all files under second argument that are not ignored by .gitignore files from first argument and below:

import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:pub/src/ignore.dart';

void main(List<String> args) {
  var root = p.normalize(args[0]);
  if (root == '.') root = '';
  var beneath = args.length > 1 ? p.normalize(args[1]) : root;
  if (beneath == '.') beneath = '';
  String resolve(String path) {
    return p.joinAll([root, ...p.posix.split(path)]);
  }

  Ignore.listFiles(
    beneath: beneath,
    listDir: (dir) => Directory(resolve(dir)).listSync().map((x) {
       final relative = p.relative(x.path, from: root);
      return p.posix.joinAll(p.split(relative));
    }),
    ignoreForDir: (dir) {
      final f = File(resolve('dir/.gitignore'));
      return f.existsSync() ? Ignore([f.readAsStringSync()]) : null;
    },
    isDir: (dir) => Directory(resolve(dir)).existsSync(),
  ).forEach(print);
}

Implementation

static List<String> listFiles({
  String beneath = '',
  required Iterable<String> Function(String) listDir,
  required Ignore? Function(String) ignoreForDir,
  required bool Function(String) isDir,
  bool includeDirs = false,
}) {
  if (beneath.startsWith('/') ||
      beneath.startsWith('./') ||
      beneath.startsWith('../')) {
    throw ArgumentError.value(
        'must be relative and normalized', 'beneath', beneath);
  }
  if (beneath.endsWith('/')) {
    throw ArgumentError.value('must not end with /', beneath);
  }
  // To streamline the algorithm we represent all paths as starting with '/'
  // and the empty path as just '/'.
  if (beneath == '.') beneath = '';
  beneath = '/$beneath';

  // Will contain all the files that are not ignored.
  final result = <String>[];
  // At any given point in the search, this will contain the Ignores from
  // directories leading up to the current entity.
  // The single `null` aligns popping and pushing in this stack with [toVisit]
  // below.
  final ignoreStack = <_IgnorePrefixPair?>[null];
  // Find all ignores between './' and [beneath] (not inclusive).

  // [index] points at the next '/' in the path.
  var index = -1;
  while ((index = beneath.indexOf('/', index + 1)) != -1) {
    final partial = beneath.substring(0, index + 1);
    if (_matchesStack(ignoreStack, partial)) {
      // A directory on the way towards [beneath] was ignored. Empty result.
      return <String>[];
    }
    final ignore = ignoreForDir(
        partial == '/' ? '.' : partial.substring(1, partial.length - 1));
    ignoreStack
        .add(ignore == null ? null : _IgnorePrefixPair(ignore, partial));
  }
  // Do a depth first tree-search starting at [beneath].
  // toVisit is a stack containing all items that are waiting to be processed.
  final toVisit = [
    [beneath]
  ];
  while (toVisit.isNotEmpty) {
    final topOfStack = toVisit.last;
    if (topOfStack.isEmpty) {
      toVisit.removeLast();
      ignoreStack.removeLast();
      continue;
    }
    final current = topOfStack.removeLast();
    // This is the version of current we present to the callbacks and in
    // [result].
    //
    // The empty path is represented as '.' and there is no leading '/'.
    final normalizedCurrent = current == '/' ? '.' : current.substring(1);
    final currentIsDir = isDir(normalizedCurrent);
    if (_matchesStack(ignoreStack, currentIsDir ? '$current/' : current)) {
      // current was ignored. Continue with the next item.
      continue;
    }
    if (currentIsDir) {
      final ignore = ignoreForDir(normalizedCurrent);
      ignoreStack.add(ignore == null
          ? null
          : _IgnorePrefixPair(
              ignore, current == '/' ? current : '$current/'));
      // Put all entities in current on the stack to be processed.
      toVisit.add(listDir(normalizedCurrent).map((x) => '/$x').toList());
      if (includeDirs) {
        result.add(normalizedCurrent);
      }
    } else {
      result.add(normalizedCurrent);
    }
  }
  return result;
}