Ejecución de comandos de shell en Java
On octubre 23, 2021 by adminIntroducción
En este artículo, vamos a echar un vistazo a cómo podemos aprovechar las clases Runtime
y ProcessBuilder
para ejecutar comandos y scripts de shell con Java.
Usamos los ordenadores para automatizar muchas cosas en nuestros trabajos diarios. Los administradores de sistemas ejecutan muchos comandos todo el tiempo, algunos de los cuales son muy repetitivos y requieren cambios mínimos entre las ejecuciones.
Este proceso también está maduro para la automatización. No hay necesidad de ejecutar todo manualmente. Utilizando Java, podemos ejecutar uno o varios comandos de shell, ejecutar scripts de shell, ejecutar el terminal/símbolo de comandos, establecer directorios de trabajo y manipular variables de entorno a través de las clases del núcleo.
Runtime.exec()
La clase Runtime
en Java es una clase de alto nivel, presente en cada una de las aplicaciones Java. A través de ella, la propia aplicación se comunica con el entorno en el que se encuentra.
Al extraer el runtime asociado a nuestra aplicación a través del método getRuntime()
, podemos utilizar 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 encommand
en un proceso separado. -
public Process exec(String command, String envp)
– Ejecuta elcommand
, con una matriz de variables de entorno. Se proporcionan como una matriz de cadenas, siguiendo el formatoname=value
. -
public Process exec(String command, String envp, File dir)
– Ejecuta elcommand
, con las variables de entorno especificadas, desde dentro del directoriodir
. -
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 directoriodir
.
Cabe destacar que estos procesos se ejecutan de forma externa al intérprete y dependerán del sistema.
También cabe destacar la diferencia entre String command
y String cmdArray
. Consiguen lo mismo. Un command
se descompone en una matriz de todos modos, por lo que el uso de cualquiera de estos dos debe producir los mismos resultados.
Es usted quien debe 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í.
Ejecución de un comando a partir de una cadena
Empecemos con el método más sencillo de estos tres:
Process process = Runtime.getRuntime().exec("ping www.stackabuse.com");
Al ejecutar este código se ejecutará el comando que hemos suministrado en formato String. Sin embargo, no vemos nada cuando ejecutamos esto.
Para validar si esto se ejecutó correctamente, vamos a querer echar mano del objeto process
. Vamos a utilizar un BufferedReader
para echar un vistazo a lo que está pasando:
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 parecido a:
Pinging www.stackabuse.com with 32 bytes of data:Reply from 104.18.57.23: bytes=32 time=21ms TTL=56Reply from 104.18.57.23: bytes=32 time=21ms TTL=56Reply from 104.18.57.23: bytes=32 time=21ms TTL=56Reply from 104.18.57.23: bytes=32 time=21ms TTL=56Ping 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 Process
a medida que avanzamos en otros ejemplos.
Especificar el directorio de trabajo
Si queremos ejecutar un comando desde, por ejemplo, una determinada carpeta, haríamos algo parecido a:
Process process = Runtime.getRuntime() .exec("cmd /c dir", null, new File("C:\Users\")); //.exec("sh -c ls", null, new File("Pathname")); for non-Windows usersprintResults(process);
Aquí, hemos dotado al método exec()
de un command
, un null
para nuevas variables de entorno y un new File()
que se establece como nuestro directorio de trabajo.
Cabe destacar la adición de cmd /c
antes de un comando como dir
.
Como estoy trabajando en Windows, esto abre el cmd
y /c
realiza el comando posterior. En este caso, es dir
.
La razón por la que esto no era obligatorio para el ejemplo de ping
, pero es obligatorio para este ejemplo es muy bien respondida por un usuario de SO.
Ejecutar el trozo de código anterior dará como resultado:
Volume in drive C has no label. Volume Serial Number is XXXX-XXXX Directory of C:\Users08/29/2019 05:01 PM <DIR> .08/29/2019 05:01 PM <DIR> ..08/18/2016 09:11 PM <DIR> Default.migrated08/29/2019 05:01 PM <DIR> Public05/15/2020 11:08 AM <DIR> User 0 File(s) 0 bytes 5 Dir(s) 212,555,214,848 bytes free
Veamos cómo podríamos suministrar el comando anterior en varias partes individuales, en lugar de una sola Cadena:
Process process = Runtime.getRuntime().exec( new String{"cmd", "/c", "dir"}, null, new File("C:\Users\")); printResults(process);
Ejecutar este trozo de código también dará como resultado:
Volume in drive C has no label. Volume Serial Number is XXXX-XXXX Directory of C:\Users08/29/2019 05:01 PM <DIR> .08/29/2019 05:01 PM <DIR> ..08/18/2016 09:11 PM <DIR> Default.migrated08/29/2019 05:01 PM <DIR> Public05/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 – utilizando una sola cadena o una matriz de cadenas, el comando que se introduce siempre se dividirá en una matriz antes de ser procesado por la lógica subyacente.
Cual de las dos opciones se reduce a la que te resulte más legible.
Usando Variables de Entorno
Veamos como podemos usar las variables de entorno:
Process process = Runtime.getRuntime().exec( "cmd /c echo %var1%", new String{"var1=value1"}); printResults(process);
Podemos suministrar tantas variables de entorno como queramos dentro de la matriz String. Aquí, acabamos de imprimir el valor de var1
usando echo
.
La ejecución de este código devolverá:
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 añadir todo programáticamente.
Dependiendo de tu sistema operativo, usarías archivos .bat
o .sh
. Vamos a crear uno con el contenido:
echo Hello World
A continuación, vamos a utilizar el mismo enfoque que antes:
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:
Con todas las firmas sobrecargadas de exec()
solucionadas, echemos un vistazo a la clase ProcessBuilder
y cómo podemos ejecutar comandos con ella.
ProcessBuilder
ProcessBuilder
es el mecanismo subyacente que ejecuta los comandos cuando usamos el método Runtime.getRuntime().exec()
:
/** * 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
Echar un vistazo a cómo el ProcessBuilder
toma nuestra entrada del método exec()
y ejecuta el comando, nos da una buena idea de cómo usarlo también.
Acepta un String cmdarray
, y eso es suficiente para que se ejecute. Alternativamente, podemos suministrarle argumentos opcionales como el String envp
y el File dir
.
Exploremos estas opciones.
ProcessBuilder: Ejecución de comandos a partir de 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 del directorio C:/Users
como antes, haríamos:
ProcessBuilder processBuilder = new ProcessBuilder();processBuilder.command("cmd", "/c", "dir C:\Users");Process process = processBuilder.start();printResults(process);
Para ejecutar realmente un Process
, ejecutamos el comando start()
y asignamos el valor devuelto a una instancia Process
.
La ejecución de este código dará como resultado:
Volume in drive C has no label. Volume Serial Number is XXXX-XXXX Directory of C:\Users08/29/2019 05:01 PM <DIR> .08/29/2019 05:01 PM <DIR> ..08/18/2016 09:11 PM <DIR> Default.migrated08/29/2019 05:01 PM <DIR> Public05/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 establecer cosas mediante programación, no sólo a través de comandos.
ProcessBuilder: Especificar el directorio de trabajo
En lugar de suministrar el directorio de trabajo a través del comando, vamos a establecerlo mediante programación:
processBuilder.command("cmd", "/c", "dir").directory(new File("C:\Users\"));
Aquí, hemos establecido que el directorio de trabajo sea el mismo que antes, pero hemos movido esa definición fuera del propio comando. La ejecución de este código proporcionará el mismo resultado que el último ejemplo.
ProcessBuilder: Variables de entorno
Usando los métodos de ProcessBuilder
es fácil recuperar una lista de variables de entorno en forma de Map
. También es fácil establecer las variables de entorno para que su programa pueda utilizarlas.
Obtengamos las variables de entorno actualmente disponibles y luego agreguemos algunas para su uso posterior:
ProcessBuilder processBuilder = new ProcessBuilder();Map<String, String> environmentVariables = processBuilder.environment();environmentVariables.forEach((key, value) -> System.out.println(key + value));
Aquí, hemos empaquetado las variables de entorno devueltas en un Map
y ejecutamos un forEach()
en él para imprimir los valores en nuestra consola.
Ejecutando este código obtendremos una lista de las variables de entorno que tenemos en nuestra máquina:
DriverDataC:\Windows\System32\Drivers\DriverDataHerokuPathE:\HerokuProgramDataC:\ProgramData...
Ahora, añadamos una variable de entorno a esa lista y utilicémosla:
environmentVariables.put("var1", "value1");processBuilder.command("cmd", "/c", "echo", "%var1%");Process process = processBuilder.start();printResults(process);
Ejecutando este código obtendremos:
value1
Por supuesto, una vez que el programa haya terminado de ejecutarse, esta variable no permanecerá en la lista.
ProcessBuilder: Ejecutando archivos .bat y .sh
Si quisieras ejecutar un archivo, de nuevo, sólo tendríamos que suministrar a la instancia ProcessBuilder
la información requerida:
processBuilder .command("cmd", "/c", "start", "file.bat") .directory(new File("C:\Users\User\Desktop"));Process process = processBuilder.start();
La ejecución de este código hace que se abra el símbolo del sistema y se ejecute el archivo .bat
:
Conclusión
En este artículo, hemos explorado ejemplos de ejecución de comandos de la shell en Java. Para ello hemos utilizado las clases Runtime
y ProcessBuilder
.
Usando Java, podemos ejecutar uno o varios comandos de shell, ejecutar scripts de shell, ejecutar el prompt de terminal/comando, establecer directorios de trabajo y manipular variables de entorno a través de las clases del núcleo.
Deja una respuesta