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

时间:2014-11-17 11:02:43

标签: swift bash shell 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 ....

14 个答案:

答案 0 :(得分:100)

如果你不在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

答案 1 :(得分:30)

这里的问题是你不能混淆和匹配Bash和Swift。您已经知道如何从命令行运行Swift脚本,现在需要添加方法以在Swift中执行Shell命令。摘自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"]);

至于搜索目录内容(这是Bash中ls的作用),我建议使用NSFileManager并直接在Swift中扫描目录,而不是Bash输出,这可能是一个痛苦的解析。

答案 2 :(得分:26)

如果您想使用命令行参数"确切地说"就像在命令行中那样(不分离所有参数),尝试以下方法。

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

ButtonGroup

答案 3 :(得分:20)

实用程序功能在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)
}

答案 4 :(得分:16)

如果您想使用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)")

答案 5 :(得分:9)

基于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"])

答案 6 :(得分:7)

更新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)
}

答案 7 :(得分:4)

尝试了此处发布的一些解决方案后,我发现执行命令的最佳方法是对参数使用-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")

答案 8 :(得分:3)

由于Apple已弃用.launchPath和launch(),因此仅对此进行了更新,这是Swift 4的更新实用程序功能,应在将来作进一步证明。

注意:Apple的替换文件(run()executableURL等)目前基本上为空。

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")

应该能够将其直接粘贴到操场上以查看其运行情况。

答案 9 :(得分:1)

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)

或使用 swift-commands

import Commands

Commands.Bash.system("ls")

答案 10 :(得分:0)

混合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
}

答案 11 :(得分:0)

对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)
}

答案 12 :(得分:0)

使用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)")
   }
}

答案 13 :(得分:0)

我已经建立了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,它还支持各种预定义的命令。