NSTask 와 NSPipe 를 이용해 쉘 커맨드 실행시켜서 결과 얻어오기

과연 이 내용이 현대(?)시대에 필요할지에 대해서 의문이 있을지도 모르겠지만 아직까지는 터미널 유틸리티의 특수성(?) 때문에 종종 쓰이고 있다고 생각된다. 그래서 이에 대해 정리해 본다.

참고) 이 내용은 iOS에서는 테스트 되지 않았다.

예제

간단히 플레이그라운드에서 테스트 해 볼 수 있는 코드 예제이다. 이 예제는 ls 를 실행시켜서 사용자의 홈 디렉토리 내용을 가져온다. 단, 아래 task.arguments 로 설정하는 디렉토리의 username 부분은 실제 사용자 아이디로 고쳐야 제대로 동작한다.
let task = NSTask()
task.launchPath = "/bin/ls"
task.arguments = ["-nf", "/Users/username"]

let pipe = NSPipe()
task.standardOutput = pipe

task.launch()
task.waitUntilExit()

let status = task.terminationStatus
if status == 0 {
  let fileHandle = pipe.fileHandleForReading
  let data = fileHandle.readDataToEndOfFile()
  
  var string = String(data: data, encoding: NSUTF8StringEncoding)
  print(string)
}

해설 -_-

UNIX 유틸리티들의 동작을 알고 있다면, 그리고 파이프 같은 용어를 알고 있다면 별 설명은 필요 없을 간단한 내용이다.
제목부터 쉘 커맨드를 실행시킨다고 표현하고 있는데 이는 사실 틀린 말이다. 명확하게 설명하자면 이 예제는 쉘을 통하지 않고 바로 실행시키기 때문에 터미널 유틸리티를 실행시켜서 결과를 가져오는 것이다. 그냥 설명 이해를 위해서 쉘 커맨드라고 표현하고 있으니 참고하자.
launchPath는 실행시킬 쉘 커맨드(정확히는 실행 파일 경로), arguments는 이 쉘 커맨드에 넘겨줄 인자이다. 이 녀석들은 아래와 같은 커맨드를 표현하고 있다.
$ /bin/ls -nf /Users/username
이렇게 만들어진 task는 launch() 메소드를 이용해 실행시킬 수 있는데 결과를 바로 얻을 수는 없다. 결과물을 얻기 위해서는 파이프(pipe)를 이용해야 한다. 여기서는 그냥 유틸리티리를 실행시키고 그 결과물을 파이프를 이용해 받아 올 수 있다.
UNIX에서 파이프(pipe) 라는 용어는 데이터의 연결 통로를 의미한다. 보통은 파이프를 이용해 입력을 파일 쓰듯이, 출력을 파일 읽듯이 사용 할 수 있다.
그래서 task.standardOutput 즉 표준출력(stdout)을 파이프(pipe)로 설정한다. 이렇게 하면 task의 실행 결과를 pipe를 이용해 파일을 읽어들이듯 읽을 수 있다. 그래서 이 후에는 fileHandler을 가져와서 파일 끝까지 읽어들인 후 이를 문자열로 변환하는 것이 전부이다. 여기서는 이 문자열을 print로 콘솔에 출력까지 하고 있는데 함수화 시키고 싶다면 이 string을 리턴시키는 형태로 고치기만 하면 될 것이다.

그 전에 task.terminationStatus 를 0과 비교하는 부분이 있는데, 전통적으로 UNIX 유틸리티은 실행이 정상적으로 끝난 경우 0 이라는 종료코드를 리턴한다. 혹시나 이상하게 생각되었다면 참고하자.
위 예제에서 task.launch() 후 waitUntilExit() 를 이용해 해당 유틸리티 실행이 끝날 때 까지 대기하는 부분이 있다. 즉 동기화(synchronize)되어 동작하므로 UI가 얼어버리기를 원치 않는다면 dispatch 등등을 이용해 비동기로 구동시키자.

댓글

이 블로그의 인기 게시물

버전(Version)을 제대로 이해하기

소수점 제거 함수 삼총사 ceil(), floor(), round()