
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!
Comments