En muchos proyectos, nos podemos encontrar con la necesidad de utilizar programas existentes por linea de comandos, cómo puede ser una utilidad para enviar correos electrónicos, openVC, telegram-cli, weather-cli, etc.

En este artículo te voy a explicar cómo podemos programar de forma correcta una pequeña utilidad en Swift para ejecutar estos comandos por linea de comandos y capturar su respuesta.

Introducción

Con el siguiente snipped de código ya podríamos ejecutar cualquier comando, pero vamos a hacerlo un poco mejor 😉

let command = "ps -aux"
let task = Process()
task.launchPath = "/bin/bash"
task.arguments = ["-c", command]
            
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
            
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String

print(output)

Primero que nada tenemos que pensar que este código va a ser ejecutado varias veces por lo que vamos a encapsularlo dentro de una función.

func run(_ command: String) -> String {
    let task = Process()
    task.launchPath = "/bin/bash"
    task.arguments = ["-c", command]
    
    let pipe = Pipe()
    task.standardOutput = pipe
    task.launch()
    
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String
    self.displayLogic?.showSomething(string: output)
    return output
}

En este momento tenemos que pensar si queremos que nuestro programa sea síncrono o asíncrono. Dependiendo de las necesidades de cada uno… Mis programas con Raspberry Pi por regla general son todos asíncronos por lo que ejecuto todas las tareas en un segundo hilo y dejo el hilo principal libre para así poder hacer varias cosas de forma concurrente.

Nuestro programa en Swift

En Swift tenemos varias formas de devolver parámetros a una invocación asíncrona, las más usadas quizás sean los Closures (completionHandler) o también mediante Protocolos. Mi forma preferida es mediante Protocolos, por lo que en este ejemplo lo haré de esta forma.

Partiremos de nuestro fichero main.swift en el que crearemos la clase Main y la instanciaremos.

class Main {
    init() {
        print("Ejecutando el constructor...")
    }
}

let _ = Main()

Una vez tengamos la clase Main creada vamos a crear el protocolo que notificará a la clase Main cuando nuestro comando por terminal haya terminado y que debe mostrar el resultado por pantalla/terminal (En este ejemplo lo voy a hacer así, lo mejor sería parsear la respuesta y actuar de una forma u otra dependiendo del caso). Lo llamaremos MainDisplayLogic

protocol MainDisplayLogic {
    func showSomething(string: String)
}

Aplicaremos el protocolo a la clase actual (aka Main) y lo implementaremos

class Main: MainDisplayLogic {
    func showSomething(string: String) {
        print(string)
    }
    
    init() {
        print("Ejecutando el constructor...")
    }
}

Una vez tengamos la clase Main preparada para recibir la invocación mediante la función showSomething(string: String) del protocolo MainDisplayLogic vamos a preparar una clase en la que realizaremos nuestra invocación vía comandos de terminal y la que alojará nuestra función run(...). Llamaremos a la clase ShellComandsManager.

Esta clase tendrá una propiedad del tipo del protocolo MainDisplayLogic declarado en el fichero main.swift y ubicaremos en ella la función antes mencionada

class ShellComandsManager {

    var displayLogic: MainDisplayLogic? = nil
    
    func run(_ command: String) {
        let task = Process()
        task.launchPath = "/bin/bash"
        task.arguments = ["-c", command]
        
        let pipe = Pipe()
        task.standardOutput = pipe
        task.launch()
        
        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String

        // Notificamos a la DisplayLogic que debe mostrar la salida de terminal
        self.displayLogic?.showSomething(string: output)
    }
}

Llegados hasta este punto tenemos tres actores fundamentales: nuestra clase Main, nuestra clase ShellComandsManager, nuestro protocolo MainDisplayLogic. Pero sí observamos el código veremos que estamos haciendo una ejecución síncrona de la función run(...), por lo que debemos mover toda ese ejecución a un hilo aparte.

Como lo hacemos? Pues mediante la siguiente invocación

DispatchQueue.global(qos: .background).async {
    ...
}

Y para trasladar nuestra ejecución al hilo principal, debemos hacerlo mediante la siguiente invocación

DispatchQueue.main.async {
    ...
}

Por lo que nos quedaría el siguiente código en ShellComandsManager.swift

class ShellComandsManager {

    var displayLogic: MainDisplayLogic? = nil
    
    func run(_ command: String) {
        DispatchQueue.global(qos: .background).async {
            let task = Process()
            task.launchPath = "/bin/bash"
            task.arguments = ["-c", command]
            
            let pipe = Pipe()
            task.standardOutput = pipe
            task.launch()
            
            let data = pipe.fileHandleForReading.readDataToEndOfFile()
            let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String
                
            DispatchQueue.main.async {
                self.displayLogic?.showSomething(string: output)
            }
        }
    }
}

Ahora con esta clase preparada ya podemos instanciarla e invocar la función run(...) des de nuestra clase Main inicializando previamente el delegado para poder capturar las invocaciones del MainDisplayLogic

protocol MainDisplayLogic {
    func showSomething(string: String)
}

class Main: MainDisplayLogic{
    func showSomething(string: String) {
        print(string)
    }
    
    init() {
        let shellManager = ShellComandsManager()
        shellManager.displayLogic = self
        shellManager.run("ps -aux")
    }
}

let _ = Main()

Espero que este artículo te haya servido, si tienes cualquier duda o sugerencia podemos hablar a través de los comentarios, ¡hasta la próxima!

Last modified: December 3, 2019

Author

Comments

Write a Reply or Comment

Your email address will not be published.