Как запустить команду терминала в быстром скрипте? (например, xcodebuild)
Я хочу заменить мои скрипты CI bash на swift. Я не могу понять, как вызвать нормальную команду терминала, такую как ls
или же xcodebuild
#!/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 ....
16 ответов
Если вы не используете выходные данные команды в коде 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")
Обновлено: для Swift3/Xcode8
Если вы хотите использовать аргументы командной строки "точно", как в командной строке (без разделения всех аргументов), попробуйте следующее.
(Этот ответ улучшен по сравнению с ответом LegoLess и может быть использован в Swift 4 Xcode 9.3)
func shell(_ command: String) -> String {
let task = Process()
task.launchPath = "/bin/bash"
task.arguments = ["-c", command]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String
return output
}
// Example usage:
shell("ls -la")
Проблема в том, что вы не можете смешивать и сочетать Bash и Swift. Вы уже знаете, как запустить скрипт Swift из командной строки, теперь вам нужно добавить методы для выполнения команд Shell в Swift. В заключение из блога PracticalSwift:
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, что может быть проблематичным при разборе.
Сервисная функция In 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)
}
Просто чтобы обновить это, так как Apple устарела как.launchPath, так и launch(), вот обновленная служебная функция для Swift 4, которая должна быть чуть более перспективной.
Примечание. Документация Apple по заменам ( run (), executetableURL и т. Д.) На данный момент практически пуста.
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")
Должен быть в состоянии вставить это непосредственно в детскую площадку, чтобы увидеть это в действии.
Если вы хотите использовать среду bash для вызова команд, используйте следующую функцию bash, которая использует исправленную версию Legoless. Мне пришлось удалить завершающий перевод строки из результата функции оболочки.
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)")
Полный сценарий на основе ответа Legoless
#!/usr/bin/env xcrun swift
import Foundation
func printShell(launchPath: String, arguments: [AnyObject] = []) {
let output = shell(launchPath, arguments:arguments)
if (output != nil) {
println(output!)
}
}
func shell(launchPath: String, arguments: [AnyObject] = []) -> String? {
let task = NSTask()
task.launchPath = launchPath
task.arguments = arguments
let pipe = NSPipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String? = NSString(data: data, encoding: NSUTF8StringEncoding)
return output
}
// > ls
// > ls -a -g
printShell("/bin/ls")
printShell("/bin/ls", arguments:["-a", "-g"])
Обновление для 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)
}
Попробовав некоторые из размещенных здесь решений, я обнаружил, что лучший способ выполнять команды - использовать -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")
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")
Я создал 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. Он протестирован и упрощает обработку ошибок.
Также есть ShellOut, который дополнительно поддерживает множество предопределенных команд.
Небольшое улучшение с поддержкой переменных env:
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)
}
Смешивание ответов Ринтаро и Леголесса для 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
}
Я видел много приложений, выполняющих команду терминала, например:
cd /Applications/Theirappname.app/Contents/Resources && do sth here
Эта команда не отличается от запуска сценария оболочки, и если приложение не находится в папке «Приложения», оно не будет выполнено правильно, потому что произойдет эта ошибка:
No such file or directory: /Applications/Theirappname.app
. Поэтому, если вы хотите запустить исполняемый файл в папке «Ресурсы», вам следует использовать этот код:
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
}
Я нахожусь в процессе рефакторинга некоторого существующего кода Objective-C, который использовал NSTask для Swift, и одна ключевая вещь, отсутствующая в других ответах, - это то, как вы должны обрабатывать большие объемы вывода stdout/stderr. Несоблюдение этого правила может привести к зависанию запущенного процесса.
Одна из команд, которые я обычно запускаю, может выдавать сотни КБ вывода как на стандартный вывод, так и на стандартный вывод.
Чтобы справиться с этим, я буферизую вывод таким образом:
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 — это просто классы-оболочки)
Пример использования класса 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)")
}
}