¿Cómo hacer que las tuberías funcionen con Runtime.exec ()?

104

Considere el siguiente código:

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

La salida del programa es:

/etc:
adduser.co

Cuando corro desde el shell, por supuesto, funciona como se esperaba:

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

Los internets me dicen que, debido al hecho de que el comportamiento de las tuberías no es multiplataforma, las mentes brillantes que trabajan en la fábrica de Java que produce Java no pueden garantizar que las tuberías funcionen.

¿Cómo puedo hacer esto?

No voy a hacer todo mi análisis utilizando construcciones de Java en lugar de grepy sed, porque si quiero cambiar el idioma, me veré obligado a volver a escribir mi código de análisis en ese idioma, lo cual es totalmente prohibido.

¿Cómo puedo hacer que Java realice la canalización y la redirección al llamar a comandos de shell?

poundifdef
fuente
Lo veo así: si lo hace con el manejo nativo de cadenas de Java, tiene garantizada la portabilidad de la aplicación Java a todas las plataformas compatibles con Java. OTOH, si lo hace con comandos de shell, es más fácil cambiar el idioma de Java, pero solo funcionará cuando esté en una plataforma POSIX. Pocas personas cambian el idioma de la aplicación en lugar de la plataforma en la que se ejecuta. Por eso creo que tu razonamiento es un poco curioso.
Janus Troelsen
En el caso específico de algo tan simple como command | grep foo, es mucho mejor ejecutar commandy filtrar de forma nativa en Java. Hace que su código sea algo más complejo, pero también reduce significativamente el consumo general de recursos y la superficie de ataque.
tripleee

Respuestas:

182

Escriba un script y ejecute el script en lugar de comandos separados.

La tubería es parte del caparazón, por lo que también puede hacer algo como esto:

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

Process p = Runtime.getRuntime().exec(cmd);
Kaj
fuente
@Kaj ¿Y si quisiera agregar opciones a lsie ls -lrt?
kaustav datta
8
@Kaj Veo que está intentando usar -c para especificar una cadena de comandos en el shell, pero no entiendo por qué tiene que hacer de esto una matriz de cadenas en lugar de una sola cadena.
David Doria
2
Si alguien está buscando una androidversión aquí, use /system/bin/shen su lugar
Nexen
25

Me encontré con un problema similar en Linux, excepto que era "ps -ef | grep someprocess".
Al menos con "ls" tiene un reemplazo de Java independiente del lenguaje (aunque más lento). P.ej.:

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", es un poco más difícil, porque Java no parece tener una API para ello.

Escuché que Sigar podría ayudarnos: https://support.hyperic.com/display/SIGAR/Home

Sin embargo, la solución más simple (como señaló Kaj) es ejecutar el comando canalizado como una matriz de cadenas. Aquí está el código 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();
}

En cuanto a por qué la matriz de cadenas funciona con tubería, mientras que una sola cadena no ... es uno de los misterios del universo (especialmente si no ha leído el código fuente). Sospecho que es porque cuando a exec se le da una sola cadena, primero la analiza (de una manera que no nos gusta). Por el contrario, cuando a exec se le da una matriz de cadenas, simplemente la pasa al sistema operativo sin analizarla.

En realidad, si nos tomamos un tiempo de un día ajetreado y miramos el código fuente (en 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 ), encontramos que eso es exactamente lo que está sucediendo:

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
fuente
Veo el mismo comportamiento con la redirección, es decir, el carácter '>'. Este análisis adicional sobre matrices de cadenas es esclarecedor
kris
No necesita tomarse un día de su apretada agenda, está escrito frente a sus ojos docs / api / java / lang / Runtime.html # exec (java.lang.String)
gpasch
6

Cree un Runtime para ejecutar cada uno de los procesos. Obtenga OutputStream del primer Runtime y cópielo en InputStream desde el segundo.

SJuan76
fuente
1
Vale la pena señalar que esto es mucho menos eficiente que una tubería nativa del sistema operativo, ya que está copiando memoria en el espacio de direcciones del proceso Java y luego regresa al proceso posterior.
Tim Boudreau
0

La respuesta aceptada de @Kaj es para linux. Este es el equivalente para Windows:

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