user2038560 user2038560 - 11 months ago 56
Linux Question

How to call bash cmd "find -L" inside Java code

I want to check whether a directory contains any broken links and return the number. I want to use command

find -L /dir/* -type d -prune -o -type l | wc -l
and it works fine in my terminal. But when I run it in my java code via
, it throws an error:

Unable to call process "find -L /dir/* -type d -prune -o -type l | wc -l ". Cause: find: paths must precede expression: |Usage: find [-H] [-L] [-P] [-Olevel] [-D help|tree|search|stat|rates|opt|exec] [path...] [expression]

I tried
, but none of them works. Any help is appreciated.

Answer Source

The pipe operator (|) is a functionality provided by a shell, like bash, not by the find command. Runtime.exec just passes all of the arguments to the command defined by the string's first word—in this case, find—and the find command has no idea what that pipe symbol is.

You can see this effect for yourself by running the command in a regular terminal window, but with the | escaped as \| so it is passed to the find command, like your Java code is doing:

find -L /dir/* -type d -prune -o -type l \| wc -l

One common solution is to pass the entire command as an argument o bash -c:

Process process = new ProcessBuilder("bash", "-c", 
    "find -L /dir/* -type d -prune -o -type l | wc -l").start();

(Runtime.exec is obsolete; its replacement is ProcessBuilder, which has more clearly defined behavior.)

A better solution is to count the lines in Java, and avoid the pipe symbol altogether:

Process process = new ProcessBuilder(
    "find", "-L", "/dir/*", "-type", "d", "-prune", "-o", "-type", "l").start();

long brokenLinkCount;
try (BufferedReader lineReader =
    new BufferedReader(new InputStreamReader(process.getInputStream()))) {

    brokenLinkCount = lineReader.lines().count();

Passing each argument individually to ProcessBuilder also has the benefit that the command will work for all paths, even those with spaces or quotes in the file name.

Finally, even though you have said you don’t want to go this route, the best approach is to do away with external commands and do it in Java:

Path dir = Paths.get("/dir");

Stream<Path> brokenLinks =
    Files.find(dir, 1, (path, attr) -> attr.isSymbolicLink())
    .filter(path -> {
        try {
            return !Files.exists(Files.readSymbolicLink(path));
        } catch (IOException e) {
            System.err.println("Couldn't read link " + path + ": " + e);
            return false;
long brokenLinkCount = brokenLinks.count();