Ejecutar comandos de Shell con Java

En este tutorial, cubriremos cómo ejecutar comandos de shell, archivos bat y sh en Java. Cubriremos ejemplos para todos los enfoques exec() y ProcessBuilder.

Introducción

En este artículo, veremos cómo podemos aprovechar las clases Runtime y ProcessBuilder para ejecutar comandos de shell y scripts con Java.

Usamos computadoras para automatizar muchas cosas en nuestro trabajo diario. Los administradores del sistema ejecutan muchos comandos todo el tiempo, algunos de los cuales son muy repetitivos y requieren cambios mínimos entre ejecuciones.

Este proceso también está maduro para la automatización. No hay necesidad de ejecutar todo manualmente. Usando Java, podemos ejecutar comandos de shell únicos o múltiples, ejecutar scripts de shell, ejecutar el símbolo del sistema/terminal, establecer directorios de trabajo y manipular variables de entorno a través de clases principales.

Runtime.exec()

La clase Runtime en Java es una clase de alto nivel, presente en todas las aplicaciones Java. A través de él, la propia aplicación se comunica con el entorno en el que se encuentra.

Al extraer el tiempo de ejecución asociado con nuestra aplicación a través del método getRuntime(), podemos usar el método exec() para ejecutar comandos directamente o ejecutar archivos .bat/.sh.

El método exec() ofrece algunas variaciones sobrecargadas:

  • public Process exec(String command) - Ejecuta el comando contenido en command en un proceso separado.
  • public Process exec(String command, String[] envp) - Ejecuta el command, con una matriz de variables de entorno. Se proporcionan como una matriz de cadenas, siguiendo el formato nombre=valor.
  • public Process exec(String command, String[] envp, File dir) - Ejecuta el comando, con las variables de entorno especificadas, desde el directorio dir.
  • public Process exec(String cmdArray[]) - Ejecuta un comando en forma de matriz de cadenas.
  • public Process exec(String cmdArray[], String[] envp) - Ejecuta un comando con las variables de entorno especificadas.
  • public Process exec(String cmdarray[], String[] envp, File dir) - Ejecuta un comando, con las variables de entorno especificadas, desde el directorio dir.

Vale la pena señalar que estos procesos se ejecutan externamente desde el intérprete y dependerán del sistema.

Lo que también vale la pena señalar es la diferencia entre String command y String cmdArray[]. Consiguen lo mismo. Un comando se divide en una matriz de todos modos, por lo que el uso de cualquiera de estos dos debería producir los mismos resultados.

Depende de usted decidir si exec("dir /folder") o exec(new String[]{"dir", "/folder"} es lo que le gustaría usar.

Escribamos algunos ejemplos para ver cómo estos métodos sobrecargados difieren entre sí.

Ejecutar un comando desde una cadena

Comencemos con el enfoque más simple de estos tres:

1
Process process = Runtime.getRuntime().exec("ping www.wikihtp.com");

Ejecutar este código ejecutará el comando que proporcionamos en formato de cadena. Sin embargo, no vemos nada cuando ejecutamos esto.

Para validar si esto funcionó correctamente, querremos obtener el objeto proceso. Usemos un BufferedReader para echar un vistazo a lo que está pasando:

1
2
3
4
5
6
7
public static void printResults(Process process) throws IOException {
    BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
    String line = "";
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

Ahora, cuando ejecutamos este método después del método exec(), debería producir algo similar a:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Pinging www.wikihtp.com [104.18.57.23] with 32 bytes of data:
Reply from 104.18.57.23: bytes=32 time=21ms TTL=56
Reply from 104.18.57.23: bytes=32 time=21ms TTL=56
Reply from 104.18.57.23: bytes=32 time=21ms TTL=56
Reply from 104.18.57.23: bytes=32 time=21ms TTL=56

Ping statistics for 104.18.57.23:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 21ms, Maximum = 21ms, Average = 21ms

Tenga en cuenta que tendremos que extraer la información del proceso de las instancias del Proceso a medida que avanzamos en otros ejemplos.

Especifique el directorio de trabajo

Si desea ejecutar un comando desde, digamos, una carpeta determinada, haríamos algo como:

1
2
3
4
Process process = Runtime.getRuntime()
        .exec("cmd /c dir", null, new File("C:\\Users\\"));
      //.exec("sh -c ls", null, new File("Pathname")); for non-Windows users
printResults(process);

Aquí, hemos proporcionado el método exec() con un comando, un null para nuevas variables de entorno y un nuevo archivo() que se establece como nuestro directorio de trabajo.

Vale la pena señalar la adición de cmd /c antes de un comando como dir.

Como estoy trabajando en Windows, esto abre cmd y /c ejecuta el siguiente comando. En este caso, es dir.

La razón por la que esto no era obligatorio para el ejemplo ping, pero es obligatorio para este ejemplo, está muy bien [contestada](https://stackoverflow.com/questions/31776546/why-does-runtime-execstring-work- para algunos pero no todos los comandos/31776547) por un usuario de SO.

Ejecutar el fragmento de código anterior dará como resultado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Volume in drive C has no label.
 Volume Serial Number is XXXX-XXXX

 Directory of C:\Users

08/29/2019  05:01 PM    <DIR>          .
08/29/2019  05:01 PM    <DIR>          ..
08/18/2016  09:11 PM    <DIR>          Default.migrated
08/29/2019  05:01 PM    <DIR>          Public
05/15/2020  11:08 AM    <DIR>          User
               0 File(s)              0 bytes
               5 Dir(s)  212,555,214,848 bytes free

Echemos un vistazo a cómo podríamos proporcionar el comando anterior en varias partes individuales, en lugar de una sola cadena:

1
2
3
4
5
6
Process process = Runtime.getRuntime().exec(
        new String[]{"cmd", "/c", "dir"},
        null, 
        new File("C:\\Users\\"));
        
printResults(process);

Ejecutar este fragmento de código también dará como resultado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Volume in drive C has no label.
 Volume Serial Number is XXXX-XXXX

 Directory of C:\Users

08/29/2019  05:01 PM    <DIR>          .
08/29/2019  05:01 PM    <DIR>          ..
08/18/2016  09:11 PM    <DIR>          Default.migrated
08/29/2019  05:01 PM    <DIR>          Public
05/15/2020  11:08 AM    <DIR>          User
               0 File(s)              0 bytes
               5 Dir(s)  212,542,808,064 bytes free

En última instancia, independientemente del enfoque: al usar una sola cadena o una matriz de cadenas, el comando que ingrese siempre se dividirá en una matriz antes de que la lógica subyacente lo procese.

Cuál le gustaría usar se reduce a cuál le resulta más legible.

Usar variables de entorno

Echemos un vistazo a cómo podemos usar las variables de entorno:

1
2
3
4
5
Process process = Runtime.getRuntime().exec(
        "cmd /c echo %var1%",
        new String[]{"var1=value1"});
        
printResults(process);

Podemos proporcionar tantas variables de entorno como deseemos dentro de la matriz String. Aquí, acabamos de imprimir el valor de var1 usando echo.

Ejecutar este código devolverá:

1
value1

Ejecución de archivos .bat y .sh

A veces, es mucho más fácil descargar todo en un archivo y ejecutar ese archivo en lugar de agregar todo mediante programación.

Dependiendo de su sistema operativo, usaría archivos .bat o .sh. Vamos a crear uno con los contenidos:

1
echo Hello World

Entonces, usemos el mismo enfoque que antes:

1
2
3
4
Process process = Runtime.getRuntime().exec(
        "cmd /c start file.bat",
        null,
        new File("C:\\Users\\User\\Desktop\\"));

Esto abrirá el símbolo del sistema y ejecutará el archivo .bat en el directorio de trabajo que hemos establecido.

Ejecutar este código seguramente da como resultado:

command prompt with hello world

Con todas las firmas exec() sobrecargadas resueltas, echemos un vistazo a la clase ProcessBuilder y cómo podemos ejecutar comandos usándola.

Generador de procesos

ProcessBuilder es el mecanismo subyacente que ejecuta los comandos cuando usamos el método Runtime.getRuntime().exec():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/**
 * Executes the specified command and arguments in a separate process with
 * the specified environment and working directory.
 *...
*/
public Process exec(String[] cmdarray, String[] envp, File dir) throws IOException {
    return new ProcessBuilder(cmdarray)
        .environment(envp)
        .directory(dir)
        .start();
}

[JavaDocs para la clase Runtime]{.small}

Echando un vistazo a cómo ProcessBuilder toma nuestra entrada del método exec() y ejecuta el comando, también nos da una buena idea de cómo usarlo.

Acepta un String[] cmdarray, y eso es suficiente para que funcione. Alternativamente, podemos proporcionarle argumentos opcionales como String[] envp y File dir.

Exploremos estas opciones.

ProcessBuilder: ejecución de comandos desde cadenas

En lugar de poder proporcionar una sola cadena, como cmd /c dir, tendremos que dividirla en este caso. Por ejemplo, si quisiéramos listar los archivos en el directorio C:/Users como antes, haríamos:

1
2
3
4
5
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("cmd", "/c", "dir C:\\Users");

Process process = processBuilder.start();
printResults(process);

Para ejecutar realmente un Proceso, ejecutamos el comando start() y asignamos el valor devuelto a una instancia de Proceso.

Ejecutar este código producirá:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 Volume in drive C has no label.
 Volume Serial Number is XXXX-XXXX

 Directory of C:\Users

08/29/2019  05:01 PM    <DIR>          .
08/29/2019  05:01 PM    <DIR>          ..
08/18/2016  09:11 PM    <DIR>          Default.migrated
08/29/2019  05:01 PM    <DIR>          Public
05/15/2020  11:08 AM    <DIR>          User
               0 File(s)              0 bytes
               5 Dir(s)  212,517,294,080 bytes free

Sin embargo, este enfoque no es mejor que el anterior. Lo que es útil con la clase ProcessBuilder es que es personalizable. Podemos configurar las cosas mediante programación, no solo a través de comandos.

ProcessBuilder: especifique el directorio de trabajo

En lugar de proporcionar el directorio de trabajo a través del comando, configurémoslo mediante programación:

1
processBuilder.command("cmd", "/c", "dir").directory(new File("C:\\Users\\"));

Aquí, hemos configurado el directorio de trabajo para que sea el mismo que antes, pero hemos sacado esa definición del comando en sí. Ejecutar este código proporcionará el mismo resultado que el último ejemplo.

ProcessBuilder: variables de entorno

Utilizando los métodos de ProcessBuilders, es fácil recuperar una lista de variables de entorno en forma de Mapa. También es fácil establecer variables de entorno para que su programa pueda usarlas.

Obtengamos las variables de entorno actualmente disponibles y luego agreguemos algunas para su uso posterior:

1
2
3
4
ProcessBuilder processBuilder = new ProcessBuilder();

Map<String, String> environmentVariables  = processBuilder.environment();
environmentVariables.forEach((key, value) -> System.out.println(key + value));

Aquí, empaquetamos las variables de entorno devueltas en un Mapa y ejecutamos forEach() en él para imprimir los valores en nuestra consola.

Ejecutar este código generará una lista de las variables de entorno que tiene en su máquina:

1
2
3
4
DriverDataC:\Windows\System32\Drivers\DriverData
HerokuPathE:\Heroku
ProgramDataC:\ProgramData
...

Ahora, agreguemos una variable de entorno a esa lista y usémosla:

1
2
3
4
5
environmentVariables.put("var1", "value1");

processBuilder.command("cmd", "/c", "echo", "%var1%");
Process process = processBuilder.start();
printResults(process);

Ejecutar este código producirá:

1
value1

Por supuesto, una vez que el programa haya terminado de ejecutarse, esta variable no permanecerá en la lista.

ProcessBuilder: ejecución de archivos .bat y .sh

Si desea ejecutar un archivo, nuevamente, proporcionaremos la instancia de ProcessBuilder con la información requerida:

1
2
3
4
processBuilder
        .command("cmd", "/c", "start", "file.bat")
        .directory(new File("C:\\Users\\User\\Desktop"));
Process process = processBuilder.start();

Al ejecutar este código, se abre el símbolo del sistema y se ejecuta el archivo .bat:

command prompt hello world bat file

Conclusión

En este artículo, hemos explorado ejemplos de ejecución de comandos de shell en Java. Hemos usado las clases Runtime y ProcessBuilder para hacer esto.

Usando Java, podemos ejecutar comandos de shell únicos o múltiples, ejecutar scripts de shell, ejecutar el símbolo del sistema/terminal, establecer directorios de trabajo y manipular variables de entorno a través de clases principales.