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.).