Tuesday, December 28, 2021

[SOLVED] Bash -c with Java Process API

Issue

I've been messing about with the java process API and ran into the following case:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    public static void main(String[] args) throws IOException, InterruptedException {
        var command = "echo foo";
        System.out.println(command);
        run(command);

        command = "bash -c 'echo bar'";
        System.out.println(command);
        run(command);
    }

    public static void run(String command) throws IOException, InterruptedException {
        Process p = Runtime.getRuntime().exec(command);
        p.waitFor();
        var br = new BufferedReader(new InputStreamReader(p.getInputStream()));
        br.lines().forEach(System.out::println);
    }
}

Why does bar not print? I've also tried other commands like systemctl poweroff and mkdir, but none seem to execute. I've also experimented with nohup, which works on its own but not with bash -c.


Solution

You call exec(String), which in turn calls exec(String,String,File), which in turn uses a StringTokenizer to chop the command line passed as a single string into an argument list, and then calls exec(String[],String,File).

However, that tokenizer just chops at spaces (it doesn't know that it's working with a command line or what shell would be involved). That means you end up with these tokens as the command: bash, -c, 'echo, foo' --- note the single quotes; Runtime.exec does not involve a shell to handle quotes (or variable substitution or such).

bash then complains about the 'echo, but you don't see that cause you only print the child process' stdout, but not stderr. Add code like this to run:

br = new BufferedReader(new InputStreamReader(p.getErrorStream()));
System.out.println("stderr:");
br.lines().forEach(System.out::println);

This gets me:

stderr:
bar': -c: line 1: unexpected EOF while looking for matching `''
bar': -c: line 2: syntax error: unexpected end of file

Now if you remove the single quotes from the call, you just get a single empty line because bash -c expects only one argument to run, which here is the echo, which prints a new line.

To fix this, you need to call the exec version that takes a String[] directly so that you control what is one argument:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    public static void main(String[] args) throws IOException, InterruptedException {
        run("echo", "foo");
        run("bash", "-c", "echo bar");
    }

    public static void run(String... command) throws IOException, InterruptedException {
        Process p = Runtime.getRuntime().exec(command);
        p.waitFor();
        var br = new BufferedReader(new InputStreamReader(p.getInputStream()));
        System.out.println("stdout:");
        br.lines().forEach(System.out::println);
 
        br = new BufferedReader(new InputStreamReader(p.getErrorStream()));
        System.out.println("stderr:");
        br.lines().forEach(System.out::println);
    }
}


Answered By - Robert