SwiftからPOSIXコマンドを呼び出す

# 概要

- 下記のSwiftへの焼き直しを行う
    - [POSIXコマンドをObjective\-Cで使用しようと試行錯誤した。 \- Qiita](https://qiita.com/IKEH/items/31d5344879cd945e216a)
- `NSTask`（Objective-C）と`Pipe`（Swift）は同一で良いみたい
    - https://developer.apple.com/documentation/foundation/process

# 参考
- [terminationStatus](https://developer.apple.com/documentation/foundation/process/1415801-terminationstatus)
- [\[Swift\] Processを使おう！\(1\)](https://qiita.com/masakihori/items/0639526a3baf0d6cb621)
- [\[Swift\] Swiftで簡単なCLIを作ってみる \(その3\) \| DevelopersIO](https://dev.classmethod.jp/articles/simply-cli-with-swift-3/)

# 実装
## 呼び出し元クラス
- エラー有無と出力文字列の両方が欲しいので、返り値に`Result<Success, Failure>`を使用する

```swift
import Foundation


enum ExecutingCommandError: Error {
    case errorStatus(status: Int32, commandResult: CommandResult)
}

struct CommandResult {
    let command: String
    let standardOutputMessage: String?
    let standardErrorMessage: String?
    
    var description: String {
        var message = "【Command】\n" + "\(command)\n"
        if let standardOutputMessage = standardOutputMessage,
           !standardOutputMessage.isEmpty {
            message += "(Standard Output)\n" + standardOutputMessage
        }
        
        if let standardErrorMessage = standardErrorMessage,
           !standardErrorMessage.isEmpty {
            message += "(Standard Error)\n" + standardErrorMessage
        }
        
        return message
    }
}


func executeCommand(command: String, currentDirectoryURL: URL?) -> Result<CommandResult, ExecutingCommandError> {
    let process = Process()
    process.launchPath = "/bin/bash"  // ワイルドカードを展開するためbashを経由する
    process.arguments = ["-c", command]
    
    let outputPipe = Pipe()
    let errorPipe = Pipe()
    process.standardOutput = outputPipe
    process.standardError = errorPipe
    process.currentDirectoryURL = currentDirectoryURL
    process.launch()
    process.waitUntilExit()
    
    while process.isRunning {
        Thread.sleep(forTimeInterval: 1.0)
    }
    
    let outputMessage = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
    let errorMessage = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
    let commandResult = CommandResult(command: command,
                                      standardOutputMessage: outputMessage,
                                      standardErrorMessage: errorMessage)
    
    let status = process.terminationStatus
    if status == 0 {
        return .success(commandResult)
    } else {
        return .failure(.errorStatus(status: status, commandResult: commandResult))
    }
}
```

## 呼び出し例

```swift
let result = executeCommand(command: "ls -la", currentDirectoryURL: Bundle.main.bundleURL)
switch result {
case .success(let commandResult):
    print(commandResult.description)
case .failure(let error):
    switch error {
    case .errorStatus(status: _, commandResult: let commandResult):
        print(commandResult.description)
    }
}
```

- 実行結果は以下の通り

```sh
【Command】
ls -la
(Standard Output)
total 0
drwxr-xr-x  3 xxxxx  staff   96 Jun 17 14:10 .
drwxr-xr-x@ 8 xxxxx  staff  256 Jun 23 17:21 ..
drwxr-xr-x  7 xxxxx  staff  224 Jun 17 14:10 Contents
```