Mac OS App 開發執行 shell 腳本的討論

遇到的問題

開發 Mac App 的過程當中,須要執行一段 shell 腳本. 下面是實現這個需求的幾種方法和相關問題的討論html

  • 使用 NSTask(Swfit 中叫作 Process) 執行 shell
  • 使用 NSAppleScript 借用 appleScript 執行 do shell script echo "echo test" 完成 shell 腳本的執行
    • NSUserAppleScript 提供了執行 AppleScript 的多線程方案

還有其餘的幾種 api 均可以執行 shell。由於以前的開發只涉及到上面提供的三種,不熟悉的 api 這裏不作討論shell

下面是三種 API 的討論和實例swift

NSTask

執行腳本時會生成一個 subProcess,體現爲支持多線程調用(能夠異步併發執行多個 shell 腳本)。和appleScript的 區別在於,當執行須要受權的 shell 腳本(sudo)時,NSTask 不會觸發受權的彈窗讓用戶去輸入本機密碼。api

傳入一個 shell 腳本所在的路徑。markdown

注: Mac 上手動建立一個 shell 腳本以後,須要執行 chmod +x shell腳本路徑 來對腳本受權,不然沒法執行多線程

typealias RunShellScriptResult = (_ executeResult: String) -> ()
private func runShellScript(_ path: String?, onComplete: @escaping RunShellScriptResult) {
    guard let path = path, FileManager.default.fileExists(atPath: path) else {
        onComplete("路徑不存在!")
        return
    }
    
    let task = Process()
    task.launchPath = path
    task.arguments = [""]
    let outPipe = Pipe()
    task.standardOutput = outPipe
    task.launch()
    let fileHandle = outPipe.fileHandleForReading
    let data = fileHandle.readDataToEndOfFile()
    let string = String.init(data: data, encoding: .utf8)
    task.waitUntilExit()
    
    // 獲取運行結果
    task.terminationHandler = { task in
        print("執行 \(path)")
        onComplete(string ?? "")
    }
}
複製代碼

Pipe 用於接受執行 shell 腳本的結果。併發

NSAppleScript

AppleScript 自己能夠作不少事。咱們能夠在 Mac 系統之下打開腳本編輯器,編輯本身的蘋果腳本。關於這塊,這裏不做贅述。 咱們使用它的目的,是爲了執行須要 sudo 受權的 shell 腳本,能夠彈出受權的提示。app

NSTask/Process不一樣,此時傳入的參數是 shell 腳本的內容的字符串。異步

do shell script "echo command" with administrator privileges 會增長受權彈窗提示。編輯器

/// 執行腳本命令
///
/// - Parameters:
///   - command: 命令行內容
///   - needAuthorize: 執行腳本時,是否須要 sudo 受權
/// - Returns: 執行結果
private func runCommand(_ command: String, needAuthorize: Bool) -> (isSuccess: Bool, executeResult: String?) {
    let scriptWithAuthorization = """
    do shell script "\(command)" with administrator privileges
    """
    
    let scriptWithoutAuthorization = """
    do shell script "\(command)"
    """
    
    let script = needAuthorize ? scriptWithAuthorization : scriptWithoutAuthorization
    let appleScript = NSAppleScript(source: script)
    
    var error: NSDictionary? = nil
    let result = appleScript!.executeAndReturnError(&error)
    if let error = error {
        print("執行 \n\(command)\n命令出錯:")
        print(error)
        return (false, nil)
    }
    
    return (true, result.stringValue)
}
複製代碼

NSUserAppleScript 解決的問題

NSAppleScript 雖然解決了受權的問題,可是他是執行在主線程上的。換言之,他不支持多線程執行。若是咱們須要同時執行幾個 shell 腳本,而前一個 shell 又是相似於 ping 這類的耗時操做,那你就只能乾等着。

我從 喵神寫的參考文章 摘錄了下面這段話

NSUserAppleScriptTask 中很好的一個東西就是結束時候的回調處理。腳本是異步執行的,因此你的用戶界面並不會被一個 (比較長) 的腳本鎖住。要當心你在結束回調中作的事情,由於它並非跑在主線程上的,因此你不能在那兒對你的用戶界面作更新。

do {
    // 這裏的 URL 是 shell 腳本的路徑
    let task = try NSUserAppleScriptTask.init(url: url) 
    task.execute(withAppleEvent: nil) { (result, error) in
        var message = result?.stringValue ?? ""
        // error.debugDescription 也是執行結果的一部分,有時候超時或執行 shell 自己返回錯誤,而咱們又須要打印這些內容的時候,就須要用到它。
        
        message = message.count == 0 ? error.debugDescription : message
    }
} catch {
    // 執行的相關錯誤
}
複製代碼
相關文章
相關標籤/搜索