Как запустить команду терминала в быстром скрипте? (например, 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)")
   }
}
Другие вопросы по тегам