Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- `PathUtils` removed, `PathPredicates` rework
- Line extension: empty string is permitted
- Filtering: split into distinct directories and files filters

---
## [0.0.4] - 2025-09-27
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,17 +276,23 @@ sorting/
## Filtering
Files and directories can be selectively included or excluded using a custom `Predicate<Path>`.

Filtering is **recursive by default**: directory's contents will always be traversed.
However, if a directory does not match and none of its children match, the directory itself will not be displayed.
Filtering is independant for files & directories. Files are filtered only if their parent directory pass the directory filter.
If none of some directory's children match, the directory is still displayed.

The `PathPredicates` class provides several ready-to-use methods for creating common predicates, as well as a builder for creating more advanced predicates.

```java
// Example: Filtering.java
var hasJavaExtensionPredicate = PathPredicates.builder().hasExtension("java").build();
Predicate<Path> excludeDirWithNoJavaFiles = dir -> !PathPredicates.hasNameEndingWith(dir, "no_java_file");
var isJavaFilePredicate = PathPredicates.builder().hasExtension("java").build();

var prettyPrinter = FileTreePrettyPrinter.builder()
.customizeOptions(options -> options.filter(hasJavaExtensionPredicate))
.build();
.customizeOptions(
options -> options
.filterDirectories(excludeDirWithNoJavaFiles)
.filterFiles(hasJavaExtensionPredicate)
)
.build();
```
```
filtering/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.github.computerdaddyguy.jfiletreeprettyprinter.example;

import io.github.computerdaddyguy.jfiletreeprettyprinter.ChildLimitBuilder;
import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter;
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathPredicates;
import io.github.computerdaddyguy.jfiletreeprettyprinter.PrettyPrintOptions.Sorts;
import java.nio.file.Path;
import java.util.function.Function;

public class CompleteExample {

public static void main(String[] args) {

var filterDir = PathPredicates.builder()
.pathTest(path -> !PathPredicates.hasName(path, ".git"))
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/.git"))
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/.github"))
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/.settings"))
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/src/example"))
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/src/test"))
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/target"))
.build();

var filterFiles = PathPredicates.builder()
.pathTest(path -> !PathPredicates.hasNameStartingWith(path, "."))
.pathTest(path -> {
if (PathPredicates.hasParentMatching(path, parent -> PathPredicates.hasName(parent, "jfiletreeprettyprinter"))) {
return PathPredicates.hasName(path, "FileTreePrettyPrinter.java");
}
return true;
})
.build();

var childLimitFunction = ChildLimitBuilder.builder()
.limit(path -> PathPredicates.hasFullPathMatchingGlob(path, "**/io/github/computerdaddyguy/jfiletreeprettyprinter/renderer"), 0)
.limit(path -> PathPredicates.hasFullPathMatchingGlob(path, "**/io/github/computerdaddyguy/jfiletreeprettyprinter/scanner"), 0)
.build();

Function<Path, String> lineExtension = path -> {
if (PathPredicates.hasName(path, "JfileTreePrettyPrinter-structure.png")) {
return "\t// This image";
} else if (PathPredicates.hasName(path, "FileTreePrettyPrinter.java")) {
return "\t// Main entry point";
} else if (PathPredicates.hasName(path, "README.md")) {
return "\t\t// You're reading at this!";
} else if (PathPredicates.hasName(path, "java")) {
return "";
}
return null;
};

var prettyPrinter = FileTreePrettyPrinter.builder()
.customizeOptions(
options -> options
.withEmojis(true)
.withCompactDirectories(true)
.filterDirectories(filterDir)
.filterFiles(filterFiles)
.withChildLimit(childLimitFunction)
.withLineExtension(lineExtension)
.sort(Sorts.DIRECTORY_FIRST)
)
.build();
var tree = prettyPrinter.prettyPrint(".");
System.out.println(tree);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@

import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter;
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathPredicates;
import java.nio.file.Path;
import java.util.function.Predicate;

public class Filtering {

public static void main(String[] args) {
Predicate<Path> excludeDirWithNoJavaFiles = dir -> !PathPredicates.hasNameEndingWith(dir, "no_java_file");
var hasJavaExtensionPredicate = PathPredicates.builder().hasExtension("java").build();

var prettyPrinter = FileTreePrettyPrinter.builder()
.customizeOptions(options -> options.filter(hasJavaExtensionPredicate))
.customizeOptions(
options -> options
.filterDirectories(excludeDirWithNoJavaFiles)
.filterFiles(hasJavaExtensionPredicate)
)
.build();

var tree = prettyPrinter.prettyPrint("src/example/resources/filtering");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,25 +297,37 @@ public PrettyPrintOptions sort(Comparator<Path> pathComparator) {

// ---------- Filtering ----------

@Nullable
private Predicate<Path> pathFilter = null;
private Predicate<Path> dirFilter = dir -> true;
private Predicate<Path> fileFilter = dir -> true;

@Override
@Nullable
public Predicate<Path> pathFilter() {
return pathFilter;
return path -> PathPredicates.isDirectory(path)
? dirFilter.test(path)
: fileFilter.test(path);
}

/**
* Use a custom filter for retain only some directories.
*
* Directories that do not pass this filter will not be displayed.
*
* @param filter The filter to apply on directories, cannot be <code>null</code>
*/
public PrettyPrintOptions filterDirectories(@Nullable Predicate<Path> filter) {
this.dirFilter = Objects.requireNonNull(filter, "filter is null");
return this;
}

/**
* Use a custom filter for retain only some files and/or directories.
* Use a custom filter for retain only some files.
*
* Files that do not pass this filter will not be displayed.
*
* Filtering is recursive by default: directory's contents will always be traversed.
* However, if a directory does not match and none of its children match, the directory itself will not be displayed.

* @param filter The filter, <code>null</code> to disable filtering
* @param filter The filter to apply on files, cannot be <code>null</code>
*/
public PrettyPrintOptions filter(@Nullable Predicate<Path> filter) {
this.pathFilter = filter;
public PrettyPrintOptions filterFiles(@Nullable Predicate<Path> filter) {
this.fileFilter = Objects.requireNonNull(filter, "filter is null");
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.Objects;
import java.util.Optional;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

@NullMarked
class DefaultTreeEntryRenderer implements TreeEntryRenderer {
Expand Down Expand Up @@ -41,25 +42,28 @@ private String renderTree(TreeEntry entry, Depth depth) {

private String renderDirectory(Depth depth, DirectoryEntry dirEntry, List<Path> compactPaths) {

Optional<String> extension = null;
boolean extensionEvaluated = false;
String extension = null;

if (options.areCompactDirectoriesUsed()
&& !depth.isRoot()
&& dirEntry.getEntries().size() == 1
&& dirEntry.getEntries().get(0) instanceof DirectoryEntry childDirEntry) {

extension = computeLineExtension(dirEntry.getDir());
if (extension.isEmpty()) {
extensionEvaluated = true;
if (extension == null) {
var newCompactPaths = new ArrayList<>(compactPaths);
newCompactPaths.add(childDirEntry.getDir());
return renderDirectory(depth, childDirEntry, newCompactPaths);
}
}

var line = lineRenderer.renderDirectoryBegin(depth, dirEntry, compactPaths);
if (extension == null) {
if (!extensionEvaluated) {
extension = computeLineExtension(dirEntry.getDir());
}
line += extension.orElse("");
line += Optional.ofNullable(extension).orElse("");

var childIt = dirEntry.getEntries().iterator();

Expand All @@ -81,16 +85,17 @@ private String renderDirectory(Depth depth, DirectoryEntry dirEntry, List<Path>
return line + childLines.toString();
}

private Optional<String> computeLineExtension(Path path) {
@Nullable
private String computeLineExtension(Path path) {
if (options.getLineExtension() == null) {
return Optional.empty();
return null;
}
return Optional.ofNullable(options.getLineExtension().apply(path));
return options.getLineExtension().apply(path);
}

private String renderFile(Depth depth, FileEntry fileEntry) {
var line = lineRenderer.renderFile(depth, fileEntry);
line += computeLineExtension(fileEntry.getFile()).orElse("");
line += Optional.ofNullable(computeLineExtension(fileEntry.getFile())).orElse("");
return line;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.github.computerdaddyguy.jfiletreeprettyprinter.scanner;

import io.github.computerdaddyguy.jfiletreeprettyprinter.PathPredicates;
import io.github.computerdaddyguy.jfiletreeprettyprinter.scanner.TreeEntry.DirectoryEntry;
import io.github.computerdaddyguy.jfiletreeprettyprinter.scanner.TreeEntry.FileEntry;
import io.github.computerdaddyguy.jfiletreeprettyprinter.scanner.TreeEntry.MaxDepthReachEntry;
Expand Down Expand Up @@ -35,14 +34,14 @@ public TreeEntry scan(Path fileOrDir) {
}

@Nullable
private TreeEntry handle(int depth, Path fileOrDir, @Nullable Predicate<Path> filter) {
private TreeEntry handle(int depth, Path fileOrDir, Predicate<Path> filter) {
return fileOrDir.toFile().isDirectory()
? handleDirectory(depth, fileOrDir, filter)
: handleFile(fileOrDir);
}

@Nullable
private TreeEntry handleDirectory(int depth, Path dir, @Nullable Predicate<Path> filter) {
private TreeEntry handleDirectory(int depth, Path dir, Predicate<Path> filter) {

if (depth >= options.getMaxDepth()) {
var maxDepthEntry = new MaxDepthReachEntry(depth);
Expand All @@ -58,15 +57,10 @@ private TreeEntry handleDirectory(int depth, Path dir, @Nullable Predicate<Path>
throw new UncheckedIOException("Unable to list files for directory: " + dir, e);
}

// Filter is active and no children match
if (depth > 0 && filter != null && childEntries.isEmpty() && !filter.test(dir)) {
return null; // Do no show this directory at all
}

return new DirectoryEntry(dir, childEntries);
}

private List<TreeEntry> handleDirectoryChildren(int depth, Path dir, Iterator<Path> pathIterator, @Nullable Predicate<Path> filter) {
private List<TreeEntry> handleDirectoryChildren(int depth, Path dir, Iterator<Path> pathIterator, Predicate<Path> filter) {

var childEntries = new ArrayList<TreeEntry>();
int maxChildEntries = options.getChildLimit().applyAsInt(dir);
Expand Down Expand Up @@ -95,39 +89,29 @@ private List<TreeEntry> handleDirectoryChildren(int depth, Path dir, Iterator<Pa
return childEntries;
}

private List<TreeEntry> handleLeftOverChildren(int depth, Iterator<Path> pathIterator, @Nullable Predicate<Path> filter) {
private List<TreeEntry> handleLeftOverChildren(int depth, Iterator<Path> pathIterator, Predicate<Path> filter) {
var childEntries = new ArrayList<TreeEntry>();

if (filter == null) {
var skippedChildren = new ArrayList<Path>();
pathIterator.forEachRemaining(skippedChildren::add);
var skippedChildren = new ArrayList<Path>();
while (pathIterator.hasNext()) {
var child = pathIterator.next();
var childEntry = handle(depth + 1, child, filter);
if (childEntry != null) { // Is null if no children file is retained by filter
skippedChildren.add(child);
}
}
if (!skippedChildren.isEmpty()) {
var childrenSkippedEntry = new SkippedChildrenEntry(skippedChildren);
childEntries.add(childrenSkippedEntry);
} else {
var skippedChildren = new ArrayList<Path>();
while (pathIterator.hasNext()) {
var child = pathIterator.next();
var childEntry = handle(depth + 1, child, filter);
if (childEntry != null) { // Is null if no children file is retained by filter
skippedChildren.add(child);
}
}
if (!skippedChildren.isEmpty()) {
var childrenSkippedEntry = new SkippedChildrenEntry(skippedChildren);
childEntries.add(childrenSkippedEntry);
}
}

return childEntries;
}

private Iterator<Path> directoryStreamToIterator(DirectoryStream<Path> childrenStream, @Nullable Predicate<Path> filter) {
var stream = StreamSupport.stream(childrenStream.spliterator(), false);
if (filter != null) {
var recursiveFilter = PathPredicates.builder().isDirectory().build().or(filter);
stream = stream.filter(recursiveFilter);
}
return stream
private Iterator<Path> directoryStreamToIterator(DirectoryStream<Path> childrenStream, Predicate<Path> filter) {
return StreamSupport
.stream(childrenStream.spliterator(), false)
.filter(filter)
.sorted(options.pathComparator())
.iterator();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import java.util.function.Predicate;
import java.util.function.ToIntFunction;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

@NullMarked
public interface ScanningOptions {
Expand All @@ -16,7 +15,6 @@ public interface ScanningOptions {

Comparator<Path> pathComparator();

@Nullable
Predicate<Path> pathFilter();

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,4 @@ void prettyPrint_by_path_and_string_are_same() {
assertThat(printer.prettyPrint(path)).isEqualTo(printer.prettyPrint(path.toString()));
}

@Test
void prettyPrintWithFilter_by_path_and_string_are_same() {
var path = FileStructures.simpleDirectoryWithFilesAndFolders(root, 3, 3);

FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder()
.customizeOptions(options -> options.filter(PathPredicates::isFile))
.build();

assertThat(printer.prettyPrint(path)).isEqualTo(printer.prettyPrint(path.toString()));
}

}
Loading
Loading