Wie funktioniert Pipes mit Runtime.exec ()?

104

Betrachten Sie den folgenden Code:

String commandf = "ls /etc | grep release";

try {

    // Execute the command and wait for it to complete
    Process child = Runtime.getRuntime().exec(commandf);
    child.waitFor();

    // Print the first 16 bytes of its output
    InputStream i = child.getInputStream();
    byte[] b = new byte[16];
    i.read(b, 0, b.length); 
    System.out.println(new String(b));

} catch (IOException e) {
    e.printStackTrace();
    System.exit(-1);
}

Die Ausgabe des Programms ist:

/etc:
adduser.co

Wenn ich von der Shell aus laufe, funktioniert es natürlich wie erwartet:

poundifdef@parker:~/rabbit_test$ ls /etc | grep release
lsb-release

Die Internetseiten sagen mir, dass die brillanten Köpfe, die in der Java-Fabrik arbeiten, die Java produziert, aufgrund der Tatsache, dass das Pipe-Verhalten nicht plattformübergreifend ist, nicht garantieren können, dass Pipes funktionieren.

Wie kann ich das machen?

Ich werde nicht das gesamte Parsen mit Java-Konstrukten durchführen und nicht, grepund sedwenn ich die Sprache ändern möchte, muss ich meinen Parsing-Code in dieser Sprache neu schreiben, was absolut unmöglich ist.

Wie kann ich Java dazu bringen, beim Aufrufen von Shell-Befehlen Piping und Umleitung durchzuführen?

Poundifdef
quelle
Ich sehe das so: Wenn Sie dies mit nativer Java-Zeichenfolgenbehandlung tun, ist die Portabilität der Java-App auf alle von Java unterstützten Plattformen garantiert. OTOH, wenn Sie es mit Shell-Befehlen tun, ist es einfacher, die Sprache von Java aus zu ändern , aber es funktioniert nur, wenn Sie sich auf einer POSIX-Plattform befinden. Nur wenige Benutzer ändern die App-Sprache und nicht die Plattform, auf der die App ausgeführt wird. Deshalb finde ich Ihre Argumentation etwas neugierig.
Janus Troelsen
Im speziellen Fall von etwas Einfachem wie command | grep fooIhnen ist es viel besser, wenn Sie nur commanddie Filterung nativ in Java ausführen und durchführen. Dies macht Ihren Code etwas komplexer, aber Sie reduzieren auch den Gesamtressourcenverbrauch und die Angriffsfläche erheblich.
Tripleee

Antworten:

182

Schreiben Sie ein Skript und führen Sie das Skript anstelle separater Befehle aus.

Pipe ist ein Teil der Shell, daher können Sie auch Folgendes tun:

String[] cmd = {
"/bin/sh",
"-c",
"ls /etc | grep release"
};

Process p = Runtime.getRuntime().exec(cmd);
Kaj
quelle
@Kaj Was passiert , wenn man wollte Optionen hinzufügen lsdh ls -lrt?
Kaustav Datta
8
@Kaj Ich sehe, dass Sie versuchen, -c zu verwenden, um eine Befehlsfolge für die Shell anzugeben, aber ich verstehe nicht, warum Sie dies zu einem Zeichenfolgenarray machen müssen, anstatt nur zu einer einzelnen Zeichenfolge?
David Doria
2
Wenn jemand hier nach einer androidVersion sucht , dann benutze /system/bin/shstattdessen
Nexen
25

Ich bin unter Linux auf ein ähnliches Problem gestoßen, außer dass es "ps -ef | grep someprocess" war.
Zumindest mit "ls" haben Sie einen sprachunabhängigen (wenn auch langsameren) Java-Ersatz. Z.B.:

File f = new File("C:\\");
String[] files = f.listFiles(new File("/home/tihamer"));
for (String file : files) {
    if (file.matches(.*some.*)) { System.out.println(file); }
}

Mit "ps" ist es etwas schwieriger, da Java keine API dafür zu haben scheint.

Ich habe gehört, dass Sigar uns möglicherweise helfen kann: https://support.hyperic.com/display/SIGAR/Home

Die einfachste Lösung (wie von Kaj hervorgehoben) besteht jedoch darin, den Befehl piped als String-Array auszuführen. Hier ist der vollständige Code:

try {
    String line;
    String[] cmd = { "/bin/sh", "-c", "ps -ef | grep export" };
    Process p = Runtime.getRuntime().exec(cmd);
    BufferedReader in =
            new BufferedReader(new InputStreamReader(p.getInputStream()));
    while ((line = in.readLine()) != null) {
        System.out.println(line); 
    }
    in.close();
} catch (Exception ex) {
    ex.printStackTrace();
}

Warum das String-Array mit Pipe funktioniert, während ein einzelner String nicht funktioniert, ist eines der Geheimnisse des Universums (insbesondere, wenn Sie den Quellcode nicht gelesen haben). Ich vermute, dass es daran liegt, dass exec, wenn es eine einzelne Zeichenfolge erhält, diese zuerst analysiert (auf eine Weise, die wir nicht mögen). Wenn exec dagegen ein String-Array erhält, wird es einfach an das Betriebssystem weitergeleitet, ohne es zu analysieren.

Eigentlich, wenn wir uns die Zeit nehmen und uns den Quellcode ansehen (unter http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/lang/) Runtime.java # Runtime.exec% 28java.lang.String% 2Cjava.lang.String []% 2Cjava.io.File% 29 ), wir finden, dass genau das passiert:

public Process  [More ...] exec(String command, String[] envp, File dir) 
          throws IOException {
    if (command.length() == 0)
        throw new IllegalArgumentException("Empty command");
    StringTokenizer st = new StringTokenizer(command);
    String[] cmdarray = new String[st.countTokens()];
    for (int i = 0; st.hasMoreTokens(); i++)
        cmdarray[i] = st.nextToken();
    return exec(cmdarray, envp, dir);
}
Tihamer
quelle
Ich sehe das gleiche Verhalten bei der Umleitung, dh das Zeichen '>'. Diese zusätzliche Analyse auf String-Arrays ist aufschlussreich
kris
Sie müssen sich keinen Tag Zeit nehmen - es steht vor Ihren Augen docs / api / java / lang / Runtime.html # exec (java.lang.String)
gpasch
6

Erstellen Sie eine Laufzeit, um jeden Prozess auszuführen. Holen Sie sich den OutputStream von der ersten Laufzeit und kopieren Sie ihn von der zweiten in den InputStream.

SJuan76
quelle
1
Es sei darauf hingewiesen, dass dies weitaus weniger effizient ist als eine os-native Pipe, da Sie Speicher in den Adressraum des Java-Prozesses kopieren und dann zum nachfolgenden Prozess zurückkehren.
Tim Boudreau
0

@ Kaj akzeptierte Antwort ist für Linux. Dies ist das Äquivalent für Windows:

String[] cmd = {
"cmd",
"/C",
"dir /B | findstr /R /C:"release""
};
Process p = Runtime.getRuntime().exec(cmd);
yelliver
quelle