Help us understand the problem. What is going on with this article?

FileVisitor でツリーっぽい表示

More than 3 years have passed since last update.

きっかけ

Javaを使ってディレクトリの階層構造をツリー形式で表示する
by @ukiuki

を見て、FileVisitor でなんとかならんかなーと思ったのがきっかけ。

結論から言うと、なんとかなったけどあまり綺麗にならない…
綺麗にするのはあきらめたけどここで供養

(File/Directory に visitor パターンはしっくりこないことが多いのでそもそもトライしてません)

コード

jdk1.8.0 & pleiades neon で開発。

自前 walk 処理版

出力例
C:\Users\heignamerican\testroom\filewalk
├─dir  0\
│  ├─123.txt
│  ├─456.txt
│  └─dir01\
│      └─dir02\
├─dir1\
│  ├─dir1-1\
│  │  └─file1-1-1.txt
│  └─file1.txt
├─dir2\
│  ├─file  2.txt
│  ├─file1.txt
│  └─ほげほげ.txt
├─dir3\
├─dir4\
│  └─dir1-1 - ショートカット.lnk
└─file0.txt

C:\Users\heignamerican\testroom\filewalk\file0.txt

C:\Users\heignamerican\testroom\filewalk\dir4\dir1-1 - ショートカット.lnk

Exception in thread "main" java.lang.NullPointerException
    at java.util.Objects.requireNonNull(Unknown Source)
    at heignamerican.files.Walking$MyFileWalker.start(Walking.java:42)
    at heignamerican.files.Walking.main(Walking.java:17)

Walking.java
package heignamerican.files;

import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

public class Walking {
    public static void main(final String[] args) throws IOException {
        new MyFileWalker(System.out).start(Paths.get("C:/Users/heignamerican/testroom/filewalk"));
        new MyFileWalker(System.out).start(Paths.get("C:/Users/heignamerican/testroom/filewalk/file0.txt"));
        new MyFileWalker(System.out).start(Paths.get("C:/Users/heignamerican/testroom/filewalk/dir4/dir1-1 - ショートカット.lnk"));
        new MyFileWalker(System.out).start(null);
    }

    public static class MyFileWalker {
        public MyFileWalker(final PrintStream out) {
            this.out = out;
        }

        private final PrintStream out;
        private StringBuilder indenter;

        /**
         * <p>
         * 指定された root 以下を tree っぽく表示する。
         *
         * <p>
         * コンストラクタで指定された {@code PrintStream} に出力する。<br>
         * SymbolicLink はたどらない(通常のファイル扱い)。<br>
         * スレッドセーフではない。<br>
         *
         * @param root
         *            null 禁止
         * @throws IOException
         */
        public void start(final Path root) throws IOException {
            Objects.requireNonNull(root);
            if (!Files.exists(root)) {
                out.printf("'%s' not found.%n", root);
                return;
            }
            out.println(root.toString());
            if (Files.isDirectory(root)) {
                indenter = new StringBuilder();
                walk(root);
            }
        }

        private static final String FILE_SEPARATOR = System.getProperty("file.separator");
        private static final String PREFIX_FOR_MIDDLE = "├─";
        private static final String PREFIX_FOR_LAST = "└─";
        private static final String INDENT_FOR_MIDDLE = "│  ";
        private static final String INDENT_FOR_LAST = "    ";

        private void walk(final Path path) throws IOException {
            final List<Path> children = Files.list(path).collect(Collectors.toList());

            final int lastIndex = children.size() - 1;
            for (int index = 0; index <= lastIndex; index++) {
                final Path child = children.get(index);
                final boolean isDirectory = Files.isDirectory(child);
                final boolean isMiddle = index != lastIndex;

                out.println(indenter.toString() + (isMiddle ? PREFIX_FOR_MIDDLE : PREFIX_FOR_LAST) + child.getFileName() + (isDirectory ? FILE_SEPARATOR : ""));

                if (isDirectory) {
                    final int oldLength = indenter.length();
                    indenter.append(isMiddle ? INDENT_FOR_MIDDLE : INDENT_FOR_LAST);
                    walk(child);
                    // if (notLast) { out.println(indenter); } // cmd の "tree /F" っぽくするなら入れたい
                    indenter.setLength(oldLength);
                }
            }
        }
    }
}

SimpleFileVisitor 版

出力例
C:\Users\heignamerican\testroom\filewalk
├─dir  0\
│ ├─123.txt
│ ├─456.txt
│ └─dir01\
│   └─dir02\
├─dir1\
│ ├─dir1-1\
│ │ └─file1-1-1.txt
│ └─file1.txt
├─dir2\
│ ├─file  2.txt
│ ├─file1.txt
│ └─ほげほげ.txt
├─dir3\
├─dir4\
│ └─dir1-1 - ショートカット.lnk
└─file0.txt

C:\Users\heignamerican\testroom\filewalk\file0.txt

C:\Users\heignamerican\testroom\filewalk\dir4\dir1-1 - ショートカット.lnk

Exception in thread "main" java.lang.NullPointerException
    at java.util.Objects.requireNonNull(Unknown Source)
    at heignamerican.files.Walking2$MyFileVisitor.start(Walking2.java:43)
    at heignamerican.files.Walking2.main(Walking2.java:19)
Walking2.java
package heignamerican.files;

import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Objects;
import java.util.Stack;

public class Walking2 {
    public static void main(final String[] args) throws IOException {
        new MyFileVisitor(System.out).start(Paths.get("C:/Users/heignamerican/testroom/filewalk"));
        new MyFileVisitor(System.out).start(Paths.get("C:/Users/heignamerican/testroom/filewalk/file0.txt"));
        new MyFileVisitor(System.out).start(Paths.get("C:/Users/heignamerican/testroom/filewalk/dir4/dir1-1 - ショートカット.lnk"));
        new MyFileVisitor(System.out).start(null);
    }

    public static class MyFileVisitor extends SimpleFileVisitor<Path> {
        public MyFileVisitor(final PrintStream out) {
            this.out = out;
        }

        private final PrintStream out;

        /**
         * <p>
         * 指定された root 以下を tree っぽく表示する。
         *
         * <p>
         * コンストラクタで指定された {@code PrintStream} に出力する。<br>
         * SymbolicLink はたどらない(通常のファイル扱い)。<br>
         * スレッドセーフではない。<br>
         *
         * @param root
         *            null 禁止
         * @throws IOException
         */
        public void start(final Path path) throws IOException {
            Objects.requireNonNull(path);
            if (!Files.exists(path)) {
                out.printf("'%s' not found.%n", path);
                return;
            }

            root = path;
            childrenCounts = new Stack<>();
            indenter = new StringBuilder();

            Files.walkFileTree(root, this);
        }

        private static final String FILE_SEPARATOR = System.getProperty("file.separator");
        private Path root;
        private Stack<Long> childrenCounts;
        private StringBuilder indenter;

        @Override
        public FileVisitResult visitFile(final Path path, final BasicFileAttributes attributes) throws IOException {
            if (path == root) {
                out.println(path);
            } else {
                out.println(indenter.toString() + getHereIndent() + path.getFileName());
                countdownChildren();
            }
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult preVisitDirectory(final Path path, final BasicFileAttributes attributes) throws IOException {
            if (path == root) {
                out.println(path);
                listupChildrenCount(path);
            } else {
                out.println(indenter.toString() + getHereIndent() + path.getFileName() + FILE_SEPARATOR);
                increseIndent();
                listupChildrenCount(path);
            }
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult postVisitDirectory(final Path path, final IOException attributes) throws IOException {
            if (path != root) {
                decreseIndent();
                countdownChildren();
            }
            return FileVisitResult.CONTINUE;
        }

        private void listupChildrenCount(final Path path) throws IOException {
            final long newer = Files.list(path).count();
            if (newer > 0) {
                childrenCounts.push(newer);
            }
        }

        private void countdownChildren() {
            final long rest = childrenCounts.pop() - 1;
            if (rest > 0) {
                childrenCounts.push(rest);
            }
        }

        private String getHereIndent() {
            return (childrenCounts.peek() > 1L) ? "├─" : "└─"; // インデントの文字数を 2 で固定する
        }

        private void increseIndent() {
            indenter.append((childrenCounts.peek() > 1L) ? "│ " : "  "); // インデントの文字数を 2 で固定する
        }

        private void decreseIndent() {
            indenter.setLength(indenter.length() - 2); // インデントの文字数を 2 で固定する
        }
    }
}

反省

  • ちょっと最後の方力尽きてます…
  • 「最後じゃない」っていう英語表現が分からず、最後まで分からない変数名
heignamerican
なんかメモるよ
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away