Come far funzionare le pipe con Runtime.exec ()?


104

Considera il codice seguente:

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

L'output del programma è:

/etc:
adduser.co

Quando corro dalla shell, ovviamente, funziona come previsto:

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

Internet mi dice che, poiché il comportamento delle pipe non è multipiattaforma, le menti brillanti che lavorano nella fabbrica Java che produce Java non possono garantire che le pipe funzionino.

Come posso fare questo?

Non ho intenzione di eseguire tutta la mia analisi utilizzando costrutti Java anziché grep esed , poiché se voglio cambiare la lingua, sarò costretto a riscrivere il mio codice di analisi in quella lingua, il che è totalmente vietato.

Come posso fare in modo che Java esegua piping e reindirizzamento quando si richiamano i comandi della shell?


La vedo così: se lo fai con la gestione delle stringhe Java nativa, ti viene garantita la portabilità dell'app Java su tutte le piattaforme supportate da Java. OTOH, se lo fai con i comandi della shell, è più facile cambiare la lingua da Java, ma funzionerà solo quando sei su una piattaforma POSIX. Poche persone cambiano il linguaggio dell'app piuttosto che la piattaforma su cui viene eseguita. Ecco perché penso che il tuo ragionamento sia un po 'curioso.
Janus Troelsen

Nel caso specifico di qualcosa di semplice come command | grep foote, stai molto meglio solo eseguendo commande facendo il filtro in modo nativo in Java. Rende il tuo codice un po 'più complesso ma riduci anche il consumo complessivo di risorse e la superficie di attacco in modo significativo.
tripla

Risposte:


182

Scrivi uno script ed esegui lo script invece di comandi separati.

Pipe è una parte della shell, quindi puoi anche fare qualcosa del genere:

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

Process p = Runtime.getRuntime().exec(cmd);

@Kaj E se volessi aggiungere opzioni a lsie ls -lrt?
kaustav datta

8
@Kaj vedo che stai cercando di usare -c per specificare una stringa di comandi alla shell, ma non capisco perché devi renderlo un array di stringhe invece di una singola stringa?
David Doria

2
Se qualcuno sta cercando la androidversione qui, usa /system/bin/shinvece
Nexen

25

Mi sono imbattuto in un problema simile in Linux, tranne che era "ps -ef | grep someprocess".
Almeno con "ls" hai una sostituzione Java indipendente dalla lingua (anche se più lenta). Per esempio.:

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

Con "ps", è un po 'più difficile, perché Java non sembra avere un'API per questo.

Ho sentito che Sigar potrebbe essere in grado di aiutarci: https://support.hyperic.com/display/SIGAR/Home

La soluzione più semplice, tuttavia, (come sottolineato da Kaj) è eseguire il comando piped come un array di stringhe. Ecco il codice completo:

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

Quanto al motivo per cui l'array String funziona con pipe, mentre una singola stringa no ... è uno dei misteri dell'universo (specialmente se non hai letto il codice sorgente). Sospetto che sia perché quando a exec viene data una singola stringa, la analizza per prima (in un modo che non ci piace). Al contrario, quando a exec viene fornito un array di stringhe, lo passa semplicemente al sistema operativo senza analizzarlo.

In realtà, se prendiamo del tempo da una giornata impegnativa e guardiamo il codice sorgente (su 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 ), troviamo che è esattamente ciò che sta accadendo:

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

Vedo lo stesso comportamento con il reindirizzamento, ovvero il carattere ">". Questa analisi extra sugli array di stringhe è illuminante
kris

Non hai bisogno di prenderti un giorno fuori dal tuo programma fitto di appuntamenti: è scritto davanti ai tuoi occhi docs / api / java / lang / Runtime.html # exec (java.lang.String)
gpasch

6

Crea un runtime per eseguire ogni processo. Ottieni OutputStream dal primo Runtime e copialo in InputStream dal secondo.


1
Vale la pena sottolineare che questo è molto meno efficiente di un pipe nativo del sistema operativo, poiché si copia la memoria nello spazio degli indirizzi del processo Java e quindi si torna al processo successivo.
Tim Boudreau

0

La risposta accettata da @Kaj è per Linux. Questo è l'equivalente per Windows:

String[] cmd = {
"cmd",
"/C",
"dir /B | findstr /R /C:"release""
};
Process p = Runtime.getRuntime().exec(cmd);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.