파이썬의 Subprocess 모듈을 사용하는 것과 비슷하게, Objective-C에서는 NSTask
를 사용하여 프로세스 내에서 다른 외부 프로세스를 실행할 수 있다. Swift로 넘어오면서 NSTask는 브릿징을 통해서 그대로 사용 가능했지만, API를 좀 더 모던하게 개선한 Process라는 별도의 클래스가 새롭게 정의되었다. Process를 사용하여 외부 프로세스의 출력 결과를 가져오는 방식은 .standardOutput
프로퍼티를 이용하여 Pipe를 통해 데이터를 받을 수 있다.
외부 프로세스가 실행되는 동안 .waitUntilExit()
를 통해서 프로세스 흐름을 블럭하고 기다리는 방법도 있지만, GUI를 사용하는 경우에는 UI가 멈춰버리기 때문에 .terminationHandler
를 사용하여 서브 프로세스 실행이 완료되었을 때, 런루프가 지정한 적절한 시점에 콜백을 실행할 수도 있다.
파이프를 통한 결과 획득
Process
객체를 생성하고 실행할 때 standardOutput
, standardInput
등의 속성에 Pipe
객체를 지정해주면, 해당 프로세스의 표준 입출력을 프로그램에서 사용할 수 있다. 프로세스는 생성한 후에 launchPath
와 arguments
를 지정하여 실행할 명령의 경로와 인자를 전달한다. 그런다음 실행(launch
)한 후에 .waitUntilExit()
를 통해서 해당 프로세스가 종료될때까지 기다렸다가 파이프에 들어온 데이터를 읽으면 된다.
import Foundation
let task = Process()
let pipe = Pipe()
task.launchPath = "/bin/ls"
task.standardOutput = pipe
try? task.run()
task.waitUntilExit()
// FileHandle.availableData: Data (Data? 타입이 아님)
if case let data = pipe.fileHandleForReading.availableData, data.count > 0 {
String(data:data, encoding:.utf8)?.split(separator:"\n").forEach{ print($0) }
}
파이프를 한 번 읽고나면 파이프는 닫히기 때문에, waitUntilExit()
를 호출해서 프로세스가 완전히 끝날 때까지 대기해야한다. GUI에서는 이 메소드가 블럭하면서 UI가 프리징될 수 있기 때문에, 핸들러를 통한 처리 방식을 사용할 것이 권장된다.
핸들러를 통한 결과 획득
Process
의 .terminationHandler
프로퍼티는 프로세스의 실행이 완료되었을 때 실행될 클로저로, 이 프로퍼티에 적절한 클로저를 지정하면 따로 블럭킹하지 않고 적절한 시점에 런루프를 통해서 결과를 처리하는 코드를 실행할 수 있다. 이 방식은 non-blocking이므로 GUI 상에서 사용한다면 더 어울리는 방식이라 할 수 있겠다.
참고로 .standardOutput
은 따로 지정하지 않아도 자동으로 Pipe
로 생성되지만, 프로퍼티 자체의 타입은 Any?
이므로 캐스팅하여 사용해야 한다.
import Foundation
let task = Process()
task.executableURL = URL(fileURLWithPath:"/bin/ls")
task.terminationHandler = { (proc: Process) in
// Process.standardOutput의 타입은 Any? 이므로 Pipe로 캐스팅해야함
if let data = (proc.standardOuput as? Pipe)?.fileHandleForReading.availableData,
data.count > 0
{
let s = String(data:data, encoding:.utf8)!
let filenames = s.split(separator:"\n")
filenames.forEach{ print($0) }
}
}
try? task.run()
task.waitUntilExit()
Process의 클래스 메소드 사용하기
Process
를 사용하여 외부 프로세스를 실행할 때, 코드블럭(클로저)를 통해서 콜백을 처리할 수 있고, .standardOutput
의 파이프 객체는 자동으로 생성된다. 따라서 클래스 메소드 run()
을 사용하여 다음과 같이 간단히 처리하는 것도 가능하다.
import Foundation
try? Process.run(URL(fileURLWithPath:"/bin/ls"), arguments:[]){ (proc) in
if let data = (proc.standardOutput as? Pipe)?
.fileHandleForReading
.availableData,
data.count > 0
{
String(data:data, encoding:.utf8)?
.split(separator:"\n")
.forEach{ print($0) }
}
}