Làm cách nào để chạy lệnh đầu cuối trong tập lệnh Swift? (ví dụ: xcodebuild)


87

Tôi muốn thay thế nhanh chóng các tập lệnh bash CI của mình. Tôi không thể tìm ra cách gọi lệnh đầu cuối bình thường như lshoặcxcodebuild

#!/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 ....

Câu trả lời:


136

Nếu bạn không sử dụng kết quả đầu ra lệnh trong mã Swift, sau đây là đủ:

#!/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")

Cập nhật: cho Swift3 / Xcode8


3
'NSTask' đã được đổi tên thành 'Process'
Mateusz.

4
Process () có còn trong Swift 4 không? Tôi đang nhận được một ký hiệu không xác định. : /
Arnaldo Capo

1
@ArnaldoCapo Nó vẫn hoạt động tốt đối với tôi! Đây là một ví dụ:#!/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")
CorPruijs

2
Tôi đã thử mà tôi nhận được: Tôi đã thử mà tôi nhận được: i.imgur.com/Ge1OOCG.png
cyber8200 12/1218

4
Quá trình hiện có sẵn trên hệ điều hành MacOS chỉ
shallowThought

87

Nếu bạn muốn sử dụng các đối số dòng lệnh "chính xác" như cách bạn làm trong dòng lệnh (mà không tách tất cả các đối số), hãy thử cách sau.

(Câu trả lời này cải thiện từ câu trả lời của LegoLess và có thể được sử dụng trong Swift 5)

import Foundation

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

    task.standardOutput = pipe
    task.arguments = ["-c", command]
    task.launchPath = "/bin/bash"
    task.launch()

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

    return output
}

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

7
Câu trả lời này thực sự nên cao hơn nhiều vì nó giải quyết được nhiều vấn đề của những câu trước.
Steven Hepting

1
+1. Cần lưu ý cho người dùng osx /bin/bashtham chiếu bash-3.2. Nếu bạn muốn sử dụng tính năng cao cấp hơn của bash, thay đổi đường dẫn ( /usr/bin/env bashthường là một lựa chọn tốt)
Aserre

Bất cứ ai có thể giúp với điều này? Các đối số không vượt qua stackoverflow.com/questions/62203978/…
mahdi

34

Vấn đề ở đây là bạn không thể trộn và kết hợp Bash và Swift. Bạn đã biết cách chạy Swift script từ dòng lệnh, bây giờ bạn cần thêm các phương thức để thực thi các lệnh Shell trong Swift. Tóm lại từ blog 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
}

Đoạn mã Swift sau sẽ thực thi xcodebuildvới các đối số và sau đó xuất ra kết quả.

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

Đối với việc tìm kiếm nội dung thư mục (đó là những gì lstrong Bash), tôi khuyên bạn nên sử dụng NSFileManagervà quét thư mục trực tiếp trong Swift, thay vì đầu ra Bash, có thể khó phân tích cú pháp.


1
Tuyệt vời - Tôi đã thực hiện một vài chỉnh sửa để thực hiện biên dịch này, tuy nhiên tôi nhận được một ngoại lệ thời gian chạy khi cố gắng gọi shell("ls", [])- 'NSInvalidArgumentException', reason: 'launch path not accessible' Có ý kiến ​​gì không?
Robert

5
NSTask không tìm kiếm tệp thực thi (sử dụng PATH của bạn từ môi trường) như trình bao. Đường dẫn khởi chạy phải là đường dẫn tuyệt đối (ví dụ: "/ bin / ls") hoặc đường dẫn liên quan đến thư mục làm việc hiện tại.
Martin R

stackoverflow.com/questions/386783/… PATH về cơ bản là một khái niệm của trình bao và không thể truy cập được.
Legoless

Tuyệt vời - nó hoạt động ngay bây giờ. Tôi đã đăng toàn bộ kịch bản + một vài sửa đổi cho hoàn chỉnh. Cảm ơn bạn.
Robert

2
Sử dụng vỏ ( "cd", "~ / Desktop /"), tôi nhận được: / usr / bin / cd: dòng 4: cd: ~ / Desktop /: Không có tập tin hoặc thư mục
Zaporozhchenko Oleksandr

21

Chức năng tiện ích trong Swift 3.0

Thao tác này cũng trả về trạng thái kết thúc nhiệm vụ và chờ hoàn thành.

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

5
import Foundationmất tích
Binarian

3
Đáng buồn thay, không dành cho iOS.
Raphael

16

Nếu bạn muốn sử dụng môi trường bash để gọi các lệnh, hãy sử dụng hàm bash sau đây sử dụng phiên bản Legoless đã được sửa chữa. Tôi đã phải xóa một dòng mới ở cuối khỏi kết quả của hàm 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)
}

Ví dụ để lấy nhánh git đang làm việc hiện tại của thư mục làm việc hiện tại:

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

12

Toàn bộ kịch bản dựa trên câu trả lời của Legoless

#!/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"])

10

Chỉ để cập nhật điều này vì Apple đã không dùng cả .launchPath và khởi chạy (), đây là một chức năng tiện ích được cập nhật cho Swift 4 sẽ là bằng chứng nhiều hơn trong tương lai.

Lưu ý: Tài liệu của Apple về các thay thế ( run () , thi hànhURL , v.v.) về cơ bản là trống tại thời điểm này.

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

Có thể dán trực tiếp cái này vào một sân chơi để xem nó hoạt động.


8

Cập nhật cho Swift 4.0 (xử lý các thay đổi đối với 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)
}

đưa ra ví dụ
Gowtham Sooryaraj

3

Sau khi thử một số giải pháp được đăng ở đây, tôi thấy rằng cách tốt nhất để thực thi các lệnh là sử dụng -ccờ cho các đối số.

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

0

Kết hợp câu trả lời của rintaro và Legoless cho 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
}

0

Cải tiến nhỏ với sự hỗ trợ cho các biến 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)
}

0

Ví dụ về việc sử dụng lớp Process để chạy một script Python.

Cũng thế:

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

bạn đặt tệp ở đâu "
upload.py

0

Tôi đã xây dựng SwiftExec , một thư viện nhỏ để chạy các lệnh như vậy:

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

Đó là một thư viện một tệp tin có thể dễ dàng sao chép vào các dự án hoặc cài đặt bằng SPM. Nó đã được thử nghiệm và đơn giản hóa việc xử lý lỗi.

Ngoài ra còn có ShellOut , hỗ trợ thêm nhiều lệnh được xác định trước.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.