38

I'm trying to use Java's ProcessBuilder class to execute a command that has a pipe in it. For example:

ls -l | grep foo

However, I get an error:

ls: |: no such file or directory

Followed by:

ls: grep: no such file or directory

Even though that command works perfectly from the command line, I can not get ProcessBuilder to execute a command that redirects its output to another.

Is there any way to accomplish this?

Matthias Braun
  • 32,039
  • 22
  • 142
  • 171
user455889
  • 793
  • 2
  • 9
  • 14
  • 1
    Note since Java 9 there's [Pipeline#startPipeline(List)](https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/lang/ProcessBuilder.html#startPipeline(java.util.List)). – Slaw Mar 31 '22 at 15:34

3 Answers3

73

This should work:

ProcessBuilder b = new ProcessBuilder("/bin/sh", "-c", "ls -l| grep foo");

To execute a pipeline, you have to invoke a shell, and then run your commands inside that shell.

dogbane
  • 266,786
  • 75
  • 396
  • 414
  • 1
    For some reason I also needed to do this for `ls /dev/sd*` IOW, `ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", "ls /dev/sd*");` works great, whereas `ProcessBuilder pb = new ProcessBuilder("ls", "/dev/sd*");` did *not* work. Guessing it has something to do with vararg interpretation of the `*` in the string... Seems like a bug to me. Could also be because of the "special" `/dev` device, not sure. – likethesky Sep 25 '12 at 22:20
  • 1
    It's the shell that expands the wildcard, not ls. If you typed `ls '/dev/sd*'` then that wouldn't work either. – Dave Griffiths Jul 23 '15 at 10:19
5

The simplest way is to invoke the shell with the command line as the parameter. After all, it's the shell which is interpreting "|" to mean "pipe the data between two processes".

Alternatively, you could launch each process separately, and read from the standard output of "ls -l", writing the data to the standard input of "grep" in your example.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
4

Since Java 9, there’s genuine support for piplines in ProcessBuilder.

So you can use

List<String> result;
List<Process> processes = ProcessBuilder.startPipeline(List.of(
        new ProcessBuilder("ls", "-l")
            .inheritIO().redirectOutput(ProcessBuilder.Redirect.PIPE),
        new ProcessBuilder("grep", "foo")
            .redirectError(ProcessBuilder.Redirect.INHERIT)
    ));
try(Scanner s = new Scanner(processes.get(processes.size() - 1).getInputStream())) {
    result = s.useDelimiter("\\R").tokens().toList();
}

to get the matching lines in a list.

Or, for Windows

List<String> result;
List<Process> processes = ProcessBuilder.startPipeline(List.of(
        new ProcessBuilder("cmd", "/c", "dir")
            .inheritIO().redirectOutput(ProcessBuilder.Redirect.PIPE),
        new ProcessBuilder("find", "\"foo\"")
            .redirectError(ProcessBuilder.Redirect.INHERIT)
    ));
try(Scanner s = new Scanner(processes.get(processes.size() - 1).getInputStream())) {
    result = s.useDelimiter("\\R").tokens().toList();
}

These examples redirect stdin of the first process and all error streams to inherit, to use the same as the Java process.

You can also call .redirectOutput(ProcessBuilder.Redirect.INHERIT) on the ProcessBuilder of the last process, to print the results directly to the console (or wherever stdout has been redirected to).

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Great answer. However, I don't think you need to call redirectOutput(PIPE) as that is the default. Similarly, inheritIO() is also not necessary. – RajV Aug 22 '23 at 15:40