如何在 Swift 脚本中运行终端命令(例如 xcodebuild)

我想用 Swift 替换我的 CI bash 脚本。我不知道如何调用像 lsxcodebuild这样的普通终端命令

#!/usr/bin/env xcrun swift


import Foundation // Works
println("Test") // Works
ls // Fails
xcodebuild -workspace myApp.xcworkspace // Fails

$ ./script.swift
./script.swift:5:1: error: use of unresolved identifier 'ls'
ls // Fails
^
... etc ....
108915 次浏览

这里的问题是,你不能混合和匹配巴什和斯威夫特。您已经知道如何从命令行运行 Swift 脚本,现在需要添加在 Swift 中执行 Shell 命令的方法。来自 实用的斯威夫特博客的总结:

func shell(_ launchPath: String, _ arguments: [String]) -> String?
{
let task = Process()
task.launchPath = launchPath
task.arguments = arguments


let pipe = Pipe()
task.standardOutput = pipe
task.launch()


let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: String.Encoding.utf8)


return output
}

下面的 Swift 代码将使用参数执行 xcodebuild,然后输出结果。

shell("xcodebuild", ["-workspace", "myApp.xcworkspace"]);

至于搜索目录内容(这是 ls在 Bash 中执行的操作) ,我建议使用 NSFileManager并在 Swift 中直接扫描目录,而不是使用 Bash 输出,后者可能很难解析。

完整的脚本基于乐高的回答

#!/usr/bin/env swift


import Foundation


func printShell(launchPath: String, arguments: [String] = []) {
let output = shell(launchPath: launchPath, arguments: arguments)


if (output != nil) {
print(output!)
}
}


func shell(launchPath: String, arguments: [String] = []) -> String? {
let task = Process()
task.launchPath = launchPath
task.arguments = arguments


let pipe = Pipe()
task.standardOutput = pipe
task.launch()


let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: String.Encoding.utf8)


return output
}


// > ls
// > ls -a -g
printShell(launchPath: "/bin/ls")
printShell(launchPath: "/bin/ls", arguments:["-a", "-g"])

如果在 Swift 代码中不使用命令输出,以下内容就足够了:

#!/usr/bin/env swift


import Foundation


@discardableResult
func shell(_ args: String...) -> Int32 {
let task = Process()
task.launchPath = "/usr/bin/env"
task.arguments = args
task.launch()
task.waitUntilExit()
return task.terminationStatus
}


shell("ls")
shell("xcodebuild", "-workspace", "myApp.xcworkspace")

更新: for Swift3/Xcode8

如果您想使用 bash 环境来调用命令,请使用以下 bash 函数,该函数使用 Legoless 的固定版本。我必须从 shell 函数的结果中删除一个尾随换行符。

Swift 3.0: (Xcode8)

import Foundation


func shell(launchPath: String, arguments: [String]) -> String
{
let task = Process()
task.launchPath = launchPath
task.arguments = arguments


let pipe = Pipe()
task.standardOutput = pipe
task.launch()


let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: String.Encoding.utf8)!
if output.characters.count > 0 {
//remove newline character.
let lastIndex = output.index(before: output.endIndex)
return output[output.startIndex ..< lastIndex]
}
return output
}


func bash(command: String, arguments: [String]) -> String {
let whichPathForCommand = shell(launchPath: "/bin/bash", arguments: [ "-l", "-c", "which \(command)" ])
return shell(launchPath: whichPathForCommand, arguments: arguments)
}

例如,获取当前工作目录的当前 git 分支:

let currentBranch = bash("git", arguments: ["describe", "--contains", "--all", "HEAD"])
print("current branch:\(currentBranch)")

Swift 3.0中的实用功能

这也会返回任务终止状态并等待完成。

func shell(launchPath: String, arguments: [String] = []) -> (String? , Int32) {
let task = Process()
task.launchPath = launchPath
task.arguments = arguments


let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)
task.waitUntilExit()
return (output, task.terminationStatus)
}

将 Rintaro 和 Legoless 的答案混合在 Swift 3中

@discardableResult
func shell(_ args: String...) -> String {
let task = Process()
task.launchPath = "/usr/bin/env"
task.arguments = args


let pipe = Pipe()
task.standardOutput = pipe


task.launch()
task.waitUntilExit()


let data = pipe.fileHandleForReading.readDataToEndOfFile()


guard let output: String = String(data: data, encoding: .utf8) else {
return ""
}
return output
}

更新 Swift 4.0(处理对 String的更改)

func shell(launchPath: String, arguments: [String]) -> String
{
let task = Process()
task.launchPath = launchPath
task.arguments = arguments


let pipe = Pipe()
task.standardOutput = pipe
task.launch()


let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: String.Encoding.utf8)!
if output.count > 0 {
//remove newline character.
let lastIndex = output.index(before: output.endIndex)
return String(output[output.startIndex ..< lastIndex])
}
return output
}


func bash(command: String, arguments: [String]) -> String {
let whichPathForCommand = shell(launchPath: "/bin/bash", arguments: [ "-l", "-c", "which \(command)" ])
return shell(launchPath: whichPathForCommand, arguments: arguments)
}

如果希望像在命令行中那样“完全”使用命令行参数(不分隔所有参数) ,请尝试以下操作。

(这个答案改进了 LegoLess 的答案,可以在 Swift 5中使用)

import Foundation


func shell(_ command: String) -> String {
let task = Process()
let pipe = Pipe()
    

task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
task.launchPath = "/bin/zsh"
task.standardInput = nil
task.launch()
    

let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
    

return output
}


// Example usage:
shell("ls -la")

更新/更安全的函数调用10/23/21: 使用上面的 shell 命令可能会遇到运行时错误,如果是这样,尝试切换到下面更新的调用。您需要在新的 shell 命令周围使用 do catch 语句,但是希望这也可以节省您搜索捕获意外错误的方法的时间。

解释 : 由于 task.laun()不是一个抛出函数,它不能被捕获,我发现它偶尔会在调用时崩溃应用程序。经过大量的互联网搜索,我发现 Process 类已经放弃了 task.unch () ,转而使用一个新的函数 task.run () ,该函数会正确地抛出错误,导致应用程序崩溃。要了解更多关于更新方法的信息,请参见: < a href = “ https://eclecticlight.co/2019/02/02/scripting-in-swift-process-dedecations/”rel = “ norefrer”> https://eclecticlight.co/2019/02/02/scripting-in-swift-process-deprecations/

import Foundation


@discardableResult // Add to suppress warnings when you don't want/need a result
func safeShell(_ command: String) throws -> String {
let task = Process()
let pipe = Pipe()
    

task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
task.executableURL = URL(fileURLWithPath: "/bin/zsh") //<--updated
task.standardInput = nil


try task.run() //<--updated
    

let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
    

return output
}

例子:

// Example usage capturing error:
do {
try safeShell("ls -la")
}
catch {
print("\(error)") //handle or silence the error here
}


// Example usage where you don't care about the error and want a nil back instead
let result = try? safeShell("ls -la")


// Example usage where you don't care about the error or the return value
try? safeShell("ls -la")

注意: 在最后一种情况下,您正在使用 try?而没有使用结果,出于某种原因,编译器仍然会警告您,即使它被标记为 @discardableResult。这种情况只发生在 try?,而不是 do-try-catch块内的 try或抛出函数内的 try。不管怎样,你都可以放心地忽略它。

只是为了更新这一点,因为苹果已经否定了这两者。LaunchPath 和 laun() ,这里有一个针对 Swift 4的更新的实用函数,它应该能够更好地证明这一点。

注意: 苹果的替身计画文档(Run ()可执行网址等)基本上是空白的。

import Foundation


// wrapper function for shell commands
// must provide full path to executable
func shell(_ launchPath: String, _ arguments: [String] = []) -> (String?, Int32) {
let task = Process()
task.executableURL = URL(fileURLWithPath: launchPath)
task.arguments = arguments


let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe


do {
try task.run()
} catch {
// handle errors
print("Error: \(error.localizedDescription)")
}


let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)


task.waitUntilExit()
return (output, task.terminationStatus)
}




// valid directory listing test
let (goodOutput, goodStatus) = shell("/bin/ls", ["-la"])
if let out = goodOutput { print("\(out)") }
print("Returned \(goodStatus)\n")


// invalid test
let (badOutput, badStatus) = shell("ls")

应该能够粘贴到一个操场直接看到它的行动。

在环境变量支持下的小改进:

func shell(launchPath: String,
arguments: [String] = [],
environment: [String : String]? = nil) -> (String , Int32) {
let task = Process()
task.launchPath = launchPath
task.arguments = arguments
if let environment = environment {
task.environment = environment
}


let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8) ?? ""
task.waitUntilExit()
return (output, task.terminationStatus)
}

使用 Process 类运行 Python 脚本的示例。

另外:

 - added basic exception handling
- setting environment variables (in my case I had to do it to get Google SDK to authenticate correctly)
- arguments














import Cocoa


func shellTask(_ url: URL, arguments:[String], environment:[String : String]) throws ->(String?, String?){
let task = Process()
task.executableURL = url
task.arguments =  arguments
task.environment = environment


let outputPipe = Pipe()
let errorPipe = Pipe()


task.standardOutput = outputPipe
task.standardError = errorPipe
try task.run()


let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()


let output = String(decoding: outputData, as: UTF8.self)
let error = String(decoding: errorData, as: UTF8.self)


return (output,error)
}


func pythonUploadTask()
{
let url = URL(fileURLWithPath: "/usr/bin/python")
let pythonScript =  "upload.py"


let fileToUpload = "/CuteCat.mp4"
let arguments = [pythonScript,fileToUpload]
var environment = ProcessInfo.processInfo.environment
environment["PATH"]="usr/local/bin"
environment["GOOGLE_APPLICATION_CREDENTIALS"] = "/Users/j.chudzynski/GoogleCredentials/credentials.json"
do {
let result = try shellTask(url, arguments: arguments, environment: environment)
if let output = result.0
{
print(output)
}
if let output = result.1
{
print(output)
}


} catch  {
print("Unexpected error:\(error)")
}
}

在尝试了这里提供的一些解决方案之后,我发现执行命令的最佳方法是对参数使用 -c标志。

@discardableResult func shell(_ command: String) -> (String?, Int32) {
let task = Process()


task.launchPath = "/bin/bash"
task.arguments = ["-c", command]


let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.launch()


let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)
task.waitUntilExit()
return (output, task.terminationStatus)
}




let _ = shell("mkdir ~/Desktop/test")

我已经构建了 SwiftExec,一个用于运行这些命令的小型库:

import SwiftExec


var result: ExecResult
do {
result = try exec(program: "/usr/bin/git", arguments: ["status"])
} catch {
let error = error as! ExecError
result = error.execResult
}


print(result.exitCode!)
print(result.stdout!)
print(result.stderr!)

它是一个单文件库,可以很容易地复制粘贴到项目或使用 SPM 安装。它经过测试并简化了错误处理。

还有 出击,它还支持各种预定义的命令。

import Foundation


enum Commands {
struct Result {
public let statusCode: Int32
public let output: String
}
  

static func run(_ command: String,
environment: [String: String]? = nil,
executableURL: String = "/bin/bash",
dashc: String = "-c") -> Result {
// create process
func create(_ executableURL: String,
dashc: String,
environment: [String: String]?) -> Process {
let process = Process()
if #available(macOS 10.13, *) {
process.executableURL = URL(fileURLWithPath: executableURL)
} else {
process.launchPath = "/bin/bash"
}
if let environment = environment {
process.environment = environment
}
process.arguments = [dashc, command]
return process
}
// run process
func run(_ process: Process) throws {
if #available(macOS 10.13, *) {
try process.run()
} else {
process.launch()
}
process.waitUntilExit()
}
// read data
func fileHandleData(fileHandle: FileHandle) throws -> String? {
var outputData: Data?
if #available(macOS 10.15.4, *) {
outputData = try fileHandle.readToEnd()
} else {
outputData = fileHandle.readDataToEndOfFile()
}
if let outputData = outputData {
return String(data: outputData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
}
return nil
}
    

let process = create(executableURL, dashc: dashc, environment: environment)
    

let outputPipe = Pipe()
process.standardOutput = outputPipe
    

let errorPipe = Pipe()
process.standardError = errorPipe
    

do {
try run(process)
      

let outputActual = try fileHandleData(fileHandle: outputPipe.fileHandleForReading) ?? ""
let errorActual = try fileHandleData(fileHandle: errorPipe.fileHandleForReading) ?? ""
      

if process.terminationStatus == EXIT_SUCCESS {
return Result(statusCode: process.terminationStatus, output: outputActual)
}
return Result(statusCode: process.terminationStatus, output: errorActual)
} catch let error {
return Result(statusCode: process.terminationStatus, output: error.localizedDescription)
}
}
}

用法

let result = Commands.run("ls")
debugPrint(result.output)
debugPrint(result.statusCode)

或使用 快速命令

import Commands


Commands.Bash.system("ls")

我看到许多应用程序运行终端命令,比如:

cd /Applications/Theirappname.app/Contents/Resources && do sth here

这个命令与运行 shell 脚本没有什么不同,如果应用程序不在 Applications 文件夹中,就不会正确执行,因为会发生这个错误: No such file or directory: /Applications/Theirappname.app。 因此,如果希望在 Resources 文件夹中运行可执行文件,应该使用以下代码:

func runExec() -> Int32 {
let task = Process()
task.arguments = [Bundle.main.url(forResource: "YourExecutablefile", withExtension: "its_extension", subdirectory: "if_exists/")!.path]
//If it does not have an extension then you just leave it empty
//You can remove subdirectory if it does not exist
task.launch()
task.waitUntilExit()
return task.terminationStatus
}

如果您的可执行文件需要一个/一些参数,代码将如下所示:

func runExec() -> Int32 {
let task = Process()
task.launchPath = "/bin/bash"
task.launchPath = Bundle.main.url(forResource: "YourExecutablefile", withExtension: "its_extension", subdirectory: "if_exists")?.path
//If it does not have an extension then you just leave it empty
//You can remove subdirectory if it does not exist
task.arguments = ["arg1","arg2"]
task.launch()
task.waitUntilExit()
return task.terminationStatus
}

我正在重构一些使用 NSTask to Swift 的 Objective-C 代码,其他答案中缺少的一个关键问题是如何处理大量的 stdout/stderr 输出。如果不这样做,似乎会导致启动过程中的挂起。

我通常启动的一个命令可以向 stdout 和 stderr 产生数百 KB 的输出。

为了解决这个问题,我这样对输出进行缓冲:

import Foundation


struct ShellScriptExecutor {


static func runScript(_ script: ShellScript) -> ShellScriptResult {
var errors: String = ""
let tempFile = copyToTempFile(script)
let process = Process()
let stdout = Pipe()
let stderr = Pipe()
var stdoutData = Data.init(capacity: 8192)
var stderrData = Data.init(capacity: 8192)


process.standardOutput = stdout
process.standardError = stderr
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
process.arguments = [tempFile]


do {
try process.run()


// Buffer the data while running
while process.isRunning {
stdoutData.append(pipeToData(stdout))
stderrData.append(pipeToData(stderr))
}


process.waitUntilExit()


stdoutData.append(pipeToData(stdout))
errors = dataToString(stderrData) + pipeToString(stderr)
}


catch {
print("Process failed for " + tempFile + ": " + error.localizedDescription)
}


// Clean up
if !tempFile.isEmpty {
do {
try FileManager.default.removeItem(atPath: tempFile)
}


catch {
print("Unable to remove " + tempFile + ": " + error.localizedDescription)
}
}


return ShellScriptResult(stdoutData, script.resultType, errors)
}


static private func copyToTempFile(_ script: ShellScript) -> String {
let tempFile: String = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString + ".sh", isDirectory: false).path


if FileManager.default.createFile(atPath: tempFile, contents: Data(script.script.utf8), attributes: nil) {
return tempFile;
}
else {
return ""
}
}


static private func pipeToString(_ pipe: Pipe) -> String {
return dataToString(pipeToData(pipe))
}


static private func dataToString(_ data: Data) -> String {
return String(decoding: data, as: UTF8.self)
}


static private func pipeToData(_ pipe: Pipe) -> Data {
return pipe.fileHandleForReading.readDataToEndOfFile()
}
}

(ShellScript 和 ShellScriptResult 只是简单的包装类)