Home > Software engineering >  Bash -c with Java Process API
Bash -c with Java Process API

Time:12-18

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.

CodePudding user response:

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);
    }
}

CodePudding user response:

Personally, I'd use something higher level like the standard ProcessBuilder class or the exec library from Apache Commons instead, as they have better support for building complicated commands to execute:

import java.io.IOException;
import org.apache.commons.exec.*;

public class ExecDemo {
    // Build and execute a command with Apache Commons Exec
    private static void demoApacheExec()
        throws IOException, ExecuteException {
        var cmd = new CommandLine("sh");
        cmd.addArgument("-c");
        cmd.addArgument("echo test 1", false);
        System.out.println("Command: "   cmd);

        var executor = new DefaultExecutor();
        executor.execute(cmd);
    }

    // Build and execute a command with ProcessBuilder
    private static void demoProcessBuilder()
        throws IOException, InterruptedException {
        // Can also take a List<String> of arguments
        var pb = new ProcessBuilder("sh", "-c", "echo test 2").inheritIO();
        System.out.println("Command: "   pb.command());

        pb.start().waitFor();
    }

    public static void main(String[] args) {
        try {
            demoApacheExec();
            demoProcessBuilder();
        } catch (Exception e) {
            System.err.println("Error executing program: "   e);
            System.exit(1);
        }
    }
}

The important thing is making each argument its own separate thing instead of relying on trying to parse a single string like a shell would (A.C.E has a CommandLine.parse() function, but even it has issues with quoting; addArgument likes to put actual quotes around arguments with spaces in them when it doesn't actually need to unless you tell it not to.).

  • Related