遇到的问题
开发 Mac App
的过程中,需要执行一段 shell
脚本. 下面是实现这个需求的几种方法和相关问题的讨论
- 使用
NSTask(Swfit 中叫做 Process)
执行shell
- 使用
NSAppleScript
借用appleScript
执行do shell script echo "echo test"
完成shell
脚本的执行-
NSUserAppleScript
提供了执行AppleScript
的多线程方案
-
还有其他的几种 api
都可以执行 shell
。因为之前的开发只涉及到上面提供的三种,不熟悉的 api
这里不做讨论
下面是三种 API 的讨论和实例
NSTask
执行脚本时会生成一个
subProcess
,体现为支持多线程调用(可以异步并发执行多个shell
脚本)。和appleScript
的 区别在于,当执行需要授权的shell
脚本(sudo
)时,NSTask
不会触发授权的弹窗让用户去输入本机密码。
传入一个 shell
脚本所在的路径。
注: 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
脚本,可以弹出授权的提示。
和 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 {
// 执行的相关错误
}