Solución a inconvenientes al ejecutar comandos de terminal desde Java en macOS

En macOS, invocar comandos de terminal mediante Java puede prseentar desafíos inesperados. Un problema frecuente es que comandos simples como cd seguido de ls pueden no funcionar como se espera, mostrando el contenido del directorio raíz en lugar del directorio objetivo. Esto ocurre porque el entorno de ejecución de Java no hereda el estado del shell, por lo que cada comando se ejecuta en un contexto aislado.

Una solución efectiva consiste en encapsular los comandos en un script de shell y ejecutarlo desde Java. Previo a la ejecución, es necesario asignar permisos de ejecución al script usando chmod. Por ejemplo, para otorgar permisos completos:

ProcessBuilder permisos = new ProcessBuilder("/bin/chmod", "777", rutaCompletaScript);
Process procesoPerm = permisos.start();
int estadoPerm = procesoPerm.waitFor();

Para ejecutar el script, se puede emplear ProcessBuilder, que ofrece mayor control sobre el entorno de ejecución. Un ejemplo modificado con variables renombradas:

String nombreScript = "tarea.sh";
String directorioScript = "/usr/local/scripts";
String[] parametros = {"--modo", "rapido", "/salida"};
ProcessBuilder constructor = new ProcessBuilder("./" + nombreScript, parametros[0], parametros[1], parametros[2]);
constructor.directory(new File(directorioScript));
int codigoSalida = 0;
try {
    Process proceso = constructor.start();
    codigoSalida = proceso.waitFor();
} catch (IOException | InterruptedException excepcion) {
    excepcion.printStackTrace();
}
if (codigoSalida != 0) {
    System.err.println("El script finalizó con errores.");
}

Alternativamente, se puede usar Runtime.exec(), aunque requiere manejar manualmente los espacios entre parámetros:

String comando = directorioScript + nombreScript + " " + parametros[0] + " " + parametros[1] + " " + parametros[2];
Process proceso = Runtime.getRuntime().exec(comando);
int estado = proceso.waitFor();

Un problema común es que la ejecución se bloquee indefinidamente. Esto sucede cuando el script produce salida estándar o de error, llenando el búfer del sistema. Para evitarlo, se debe consumir el flujo de salida antes de esperar la finalización:

ProcessBuilder constructor = new ProcessBuilder("./" + nombreScript, parametros[0]);
constructor.redirectErrorStream(true);
Process proceso = constructor.start();
BufferedReader lector = new BufferedReader(new InputStreamReader(proceso.getInputStream()));
String linea;
while ((linea = lector.readLine()) != null) {
    System.out.println("Salida: " + linea);
}
int codigo = proceso.waitFor();

Otro inconveniente frecuente es que comandos funcionales manualmente fallen con "orden no encontrada" al ejecutarse desde Java. Esto se debe a que el proceso Java hereda variables de entorno limitadas. La solución consiste en crear enlaces simbólicos en /bin para los comandos necesarios. Por ejemplo, para herramientas como casperjs:

String[] enlaces = {"ln -s /opt/casperjs/bin/casperjs /bin/casperjs", "ln -s /opt/node/bin/node /bin/node"};
for (String enlace : enlaces) {
    Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", enlace}).waitFor();
}

Al trabajar con operaciones de archivo como tar, es crucial especificar rutas correctamente. Para comprimir un directorio, se debe usar la opción -C para cambiar al directorio base:

ProcessBuilder pb = new ProcessBuilder("tar", "-zcf", "/tmp/resultado.tar.gz", "-C", "/datos/", "subdirectorio");
Process p = pb.start();
p.waitFor();

Si el script de shell está contenido dentro de un archivo JAR, es necesraio extraerlo a un archivo temporal antes de ejecutarlo. Se puede acceder al recurso y copiarlo a un archivo local:

InputStream flujo = getClass().getResourceAsStream("/scripts/mi_script.sh");
File archivoTemporal = File.createTempFile("temp_script", ".sh");
try (FileOutputStream salida = new FileOutputStream(archivoTemporal)) {
    byte[] buffer = new byte[1024];
    int leido;
    while ((leido = flujo.read(buffer)) != -1) {
        salida.write(buffer, 0, leido);
    }
}
archivoTemporal.setExecutable(true);

Finalmente, si se utiliza un entorno de desarrollo como IntelliJ IDEA, pueden surgir conflictos de puertos al compilar proyectos. Una solución es ejecutar la aplicación como un archivo WAR desplegado en un servidor como Tomcat, liberando los recursos del IDE.

Etiquetas: java macos terminal shell-scripting processbuilder

Publicado el 6-18 20:55