Список всех файлов из каталога, соответствующих маске файла (также известной как Pattern или Glob)

Я хочу перечислить все файлы в каталоге и подкаталогах в этом каталоге, которые соответствуют маске файла.

Например, "M:\SOURCE\*. Doc", а SOURCE может выглядеть так:

|-- SOURCE
|   |-- Folder1
|   |   |-- File1.doc
|   |   |-- File1.txt
|   |-- File2.doc
|   |-- File3.xml

Должен возвращать File1.doc и File2.doc.

Первоначально я использую DirectoryStream, потому что он уже выполняет некоторые проверки синтаксиса маски / глобуса, а также позволяет использовать его для фильтрации, поскольку это НЕ просто регулярное выражение, а фактическая маска файла, которую обычный пользователь находит более понятной.

Files.newDirectoryStream(path, mask);

Проблема в том, что DirectoryStream проверяет только указанный вами каталог непосредственного пути, а не его подкаталоги.

ТОГДА прибывает метод "сглаживания" с Files.walk, который на самом деле может просматривать все подкаталоги, проблема в том, что он НЕ предоставляет возможности "фильтровать" маской файла так же, как DirectoryStream.

Files.walk(path, Integer.MAX_VALUE);

Так что я застрял, не могу сочетать здесь лучшее из обоих методов...

3 ответа

Решение

Я думаю, что мог бы решить свой собственный вопрос с помощью полученного здесь понимания и других вопросов, касающихся PathMatcher объект

final PathMatcher maskMatcher = FileSystems.getDefault()
                  .getPathMatcher("glob:" + mask);

final List<Path> matchedFiles = Files.walk(path)
                  .collect(Collectors.toList());

final List<Path> filesToRemove = new ArrayList<>(matchedFiles.size());

matchedFiles.forEach(foundPath -> {
            if (!maskMatcher.matches(foundPath.getFileName()) || Files.isDirectory(foundPath)) {
              filesToRemove.add(foundPath);
            }
          });

 matchedFiles.removeAll(filesToRemove);

Так что в основном .getPathMatcher("glob:" + mask);то же самое, что DirectoryStream делал для фильтрации файлов

Все, что мне нужно сделать после этого, - это отфильтровать список путей, которые я получаю с помощью Files.walk, путем удаления элементов, которые не соответствуют моему PathMatcher и не относятся к типу File

Вы также можете использовать индивидуальные FileVisitor [1], с комбинацией PathMatcher [2], который отлично работает с GLOB.

Код может выглядеть так:

public static void main(String[] args) throws IOException {
    System.out.println(getFiles(Paths.get("/tmp/SOURCE"), "*.doc"));
}

public static List<Path> getFiles(final Path directory, final String glob) throws IOException {
    final var docFileVisitor = new GlobFileVisitor(glob);
    Files.walkFileTree(directory, docFileVisitor);

    return docFileVisitor.getMatchedFiles();
}

public static class GlobFileVisitor extends SimpleFileVisitor<Path> {

    private final PathMatcher pathMatcher;
    private List<Path> matchedFiles = new ArrayList<>();

    public GlobFileVisitor(final String glob) {
        this.pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + glob);
    }

    @Override
    public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) throws IOException {
        if (pathMatcher.matches(path.getFileName())) {
            matchedFiles.add(path);
        }
        return FileVisitResult.CONTINUE;
    }

    public List<Path> getMatchedFiles() {
        return matchedFiles;
    }
}

[1] https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/FileVisitor.html

[2] https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/PathMatcher.html

Можно использовать обычный поток filter получить отфильтрованные имена файлов из Files.walk с помощью String::matches с соответствующим регулярным выражением:

final String SOURCE_DIR = "test";

Files.walk(Paths.get(SOURCE_DIR));
     .filter(p -> p.getFileName().toString().matches(".*\\.docx?"))
     .forEach(System.out::println);

Вывод

test\level01\level11\test.doc
test\level02\test-level2.doc
test\t1.doc
test\t3.docx

Структура входного каталога:

│   t1.doc
│   t2.txt
│   t3.docx
│   t4.bin
│
├───level01
│   │   test.do
│   │
│   └───level11
│           test.doc
│
└───level02
        test-level2.doc

Обновить

Рекурсивное решение возможно с использованием newDirectoryStream однако его нужно преобразовать в Stream:

static Stream<Path> readFilesByMaskRecursively(Path start, String mask) {
        
    List<Stream<Path>> sub = new ArrayList<>();
        
    try {
        sub.add(StreamSupport.stream( // read files by mask in current dir
                Files.newDirectoryStream(start, mask).spliterator(), false));
            
        Files.newDirectoryStream(start, (path) -> path.toFile().isDirectory())
             .forEach(path -> sub.add(recursive(path, mask)));
    } catch (IOException ioex) {
        ioex.printStackTrace();
    }
        
    return sub.stream().flatMap(s -> s); // convert to Stream<Path>
}

// test
readFilesByMaskRecursively(Paths.get(SOURCE_DIR), "*.doc*")
             .forEach(System.out::println);

Вывод:

test\t1.doc
test\t3.docx
test\level01\level11\test.doc
test\level02\test-level2.doc

Обновление 2

Префикс **/ может быть добавлен к PathMatcher чтобы пересечь границы каталога, затем Files.walkРешение на основе может использовать упрощенный фильтр без необходимости удаления определенных записей:

String mask = "*.doc*";
PathMatcher maskMatcher = FileSystems.getDefault().getPathMatcher("glob:**/" + mask);
Files.walk(Paths.get(SOURCE_DIR))
     .filter(path -> maskMatcher.matches(path))
     .forEach(System.out::println);

Вывод (такой же, как в рекурсивном решении):

test\level01\level11\test.doc
test\level02\test-level2.doc
test\t1.doc
test\t3.docx
Другие вопросы по тегам