2
votes

Liste tous les fichiers d'un répertoire qui correspondent à un masque de fichier (aka Pattern ou Glob)

Je souhaite lister tous les fichiers d'un répertoire et les sous-répertoires de ce répertoire qui correspondent à un masque de fichier .

Par exemple "M: \ SOURCE \ *. Doc" tandis que SOURCE peut ressembler à ceci:

Files.walk(path, Integer.MAX_VALUE);

Doit renvoyer File1.doc et File2.doc.

Au départ, j'utilise un DirectoryStream , car cela vérifie déjà la syntaxe du masque / glob ainsi que de pouvoir l'utiliser pour le filtrage car ce n'est pas juste une expression régulière, mais un masque de fichier réel qu'un utilisateur régulier trouve plus facile à comprendre

Files.newDirectoryStream(path, mask);

Le problème est qu'un DirectoryStream ne vérifie que le répertoire de chemin immédiat que vous fournissez et non ses sous-répertoires

ALORS vient une méthode "d'aplatissement" avec Files.walk qui est en fait capable de regarder à travers tous les sous-répertoires, le problème est, il NE fournit PAS la possibilité de "filtrer" par un masque de fichier de la même manière qu'un DirectoryStream

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

Je suis donc bloqué, incapable de combiner le meilleur des deux méthodes ici ...


0 commentaires

3 Réponses :


1
votes

Il est possible d'utiliser un filter flux commun pour récupérer les noms de fichiers filtrés à partir de Files.walk utilisant String::matches avec l'expression régulière appropriée:

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

Production

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);

Structure du répertoire d'entrée:

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

Mise à jour

Une solution récursive est possible en utilisant newDirectoryStream mais elle doit être convertie en 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);

Production:

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

Mise à jour 2

Un préfixe **/ peut être ajouté au PathMatcher pour traverser les limites du répertoire, puis la solution basée sur Files.walk peut utiliser un filtre simplifié sans qu'il soit nécessaire de supprimer des entrées spécifiques:

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

Sortie (identique à celle de la solution récursive):

final String SOURCE_DIR = "test";

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


2 commentaires

"avec une expression régulière appropriée", eh bien ... c'est un problème, je ne veux pas avoir à m'occuper d'expressions régulières, je veux que le masque de fichier que l'utilisateur saisisse fonctionne tout de suite. J'ai mentionné l'exemple "* .doc" mais ce n'est pas le seul masque de fichier qui peut être utilisé, avec cela, je devrai convertir chacun d'entre eux en une expression régulière appropriée.


ajout d'une solution récursive utilisant un masque de fichier commun



2
votes

Vous pouvez également utiliser FileVisitor [1] personnalisé, avec une combinaison de PathMatcher [2], qui fonctionne parfaitement avec les GLOB.

Le code pourrait ressembler à ceci:

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


0 commentaires

1
votes

Je pense que j'aurais peut-être résolu ma propre question avec les informations reçues ici et d'autres questions mentionnant l'objet 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);

Donc, fondamentalement .getPathMatcher("glob:" + mask); est la même chose que le DirectoryStream faisait pour filtrer les fichiers

Il ne me reste plus qu'à filtrer la liste des chemins que j'obtiens avec Files.walk en supprimant les éléments qui ne correspondent pas à mon PathMatcher et qui ne sont pas de type File


0 commentaires