Bagaimana cara menjalankan perintah terminal dalam skrip Swift? (mis. xcodebuild)

91

Saya ingin mengganti skrip CI bash saya dengan swift. Saya tidak tahu cara menjalankan perintah terminal normal seperti lsatauxcodebuild

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

Jawaban:

138

Jika Anda tidak menggunakan keluaran perintah dalam kode Swift, berikut ini sudah cukup:

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

Diperbarui: untuk Swift3 / Xcode8

rintaro
sumber
3
'NSTask' telah diubah namanya menjadi 'Process'
Mateusz
4
Apakah Process () masih di Swift 4? Saya mendapatkan simbol yang tidak ditentukan. : /
Arnaldo Capo
1
@ArnaldoCapo Masih berfungsi dengan baik untuk saya! Berikut ini contohnya:#!/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
Saya mencoba yang saya dapatkan: Saya mencoba yang saya dapatkan: i.imgur.com/Ge1OOCG.png
cyber8200
4
Proses hanya tersedia di macOS
shallowThought
93

Jika Anda ingin menggunakan argumen baris perintah "persis" seperti yang Anda lakukan pada baris perintah (tanpa memisahkan semua argumen), coba yang berikut ini.

(Jawaban ini meningkatkan jawaban LegoLess dan dapat digunakan di 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/zsh"
    task.launch()
    
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output = String(data: data, encoding: .utf8)!
    
    return output
}

// Example usage:
shell("ls -la")
pengguna3064009
sumber
7
Jawaban ini benar-benar harus jauh lebih tinggi karena memecahkan banyak masalah dari yang sebelumnya.
Steven Hepting
1
+1. Ini harus diperhatikan untuk pengguna osx yang /bin/bashmerujuk ke bash-3.2. Jika Anda ingin menggunakan fitur-fitur bash yang lebih canggih, ubah jalurnya ( /usr/bin/env bashbiasanya merupakan alternatif yang baik)
Aserre
Ada yang bisa membantu dengan ini? Argumen tidak lolos stackoverflow.com/questions/62203978/…
mahdi
34

Masalahnya di sini adalah Anda tidak dapat mencampur dan mencocokkan Bash dan Swift. Anda sudah tahu cara menjalankan skrip Swift dari baris perintah, sekarang Anda perlu menambahkan metode untuk menjalankan perintah Shell di Swift. Ringkasan dari 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
}

Kode Swift berikut akan dijalankan xcodebuilddengan argumen dan kemudian menampilkan hasilnya.

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

Untuk mencari konten direktori (yang lsdilakukan di Bash), saya sarankan untuk menggunakan NSFileManagerdan memindai direktori secara langsung di Swift, daripada keluaran Bash, yang bisa sulit untuk diurai.

Tanpa kaki
sumber
1
Bagus - Saya melakukan beberapa pengeditan untuk membuat kompilasi ini, namun saya mendapatkan pengecualian waktu proses saat mencoba memanggil shell("ls", [])- 'NSInvalidArgumentException', reason: 'launch path not accessible' Ada ide?
Robert
5
NSTask tidak mencari file yang dapat dieksekusi (menggunakan PATH Anda dari lingkungan) seperti yang dilakukan shell. Jalur peluncuran harus berupa jalur absolut (mis. "/ Bin / ls") atau jalur yang berhubungan dengan direktori kerja saat ini.
Martin R
stackoverflow.com/questions/386783/… PATH pada dasarnya adalah konsep shell dan tidak dapat dijangkau.
Tanpa kaki
Bagus - ini berfungsi sekarang. Saya memposting skrip lengkap + beberapa modifikasi untuk kelengkapan. Terima kasih.
Robert
2
Menggunakan shell ("cd", "~ / Desktop /"), saya mendapatkan: / usr / bin / cd: baris 4: cd: ~ / Desktop /: Tidak ada file atau direktori semacam itu
Zaporozhchenko Oleksandr
22

Fungsi utilitas di Swift 3.0

Ini juga mengembalikan status penghentian tugas dan menunggu sampai selesai.

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)
}
Arun
sumber
5
import Foundationhilang
Binarian
3
Sayangnya, tidak untuk iOS.
Raphael
17

Jika Anda ingin menggunakan lingkungan bash untuk memanggil perintah, gunakan fungsi bash berikut yang menggunakan Legoless versi tetap. Saya harus menghapus baris baru yang tertinggal dari hasil fungsi 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)
}

Misalnya untuk mendapatkan cabang git yang berfungsi saat ini dari direktori kerja saat ini:

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

Skrip lengkap berdasarkan jawaban 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"])
Robert
sumber
11

Hanya untuk memperbarui ini karena Apple telah menghentikan .launchPath dan launch (), berikut adalah fungsi utilitas yang diperbarui untuk Swift 4 yang seharusnya menjadi bukti masa depan yang sedikit lebih baik.

Catatan: Dokumentasi Apple tentang penggantian ( run () , executableURL , dll) pada dasarnya kosong pada saat ini.

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

Harus bisa menempelkannya langsung ke taman bermain untuk melihatnya beraksi.

angusc
sumber
8

Memperbarui untuk Swift 4.0 (menangani perubahan pada 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)
}
rougeExciter
sumber
berikan contoh
Gowtham Sooryaraj
4

Setelah mencoba beberapa solusi yang diposting di sini, saya menemukan bahwa cara terbaik untuk menjalankan perintah adalah menggunakan -cflag untuk argumen.

@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")
lojals
sumber
0

Menggabungkan rintaro dan jawaban Legoless untuk 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
}
kaya
sumber
0

Perbaikan kecil dengan dukungan untuk variabel 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)
}
Aliaksandr Bialiauski
sumber
0

Contoh penggunaan kelas Proses untuk menjalankan skrip Python.

Juga:

 - 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)")
   }
}
Janusz Chudzynski
sumber
di mana Anda menempatkan file "upload.py '
Suhaib Roomy
0

Saya telah membangun SwiftExec , perpustakaan kecil untuk menjalankan perintah seperti itu:

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

Ini adalah pustaka file tunggal yang dapat dengan mudah disalin-tempel ke dalam proyek atau diinstal menggunakan SPM. Ini diuji dan menyederhanakan penanganan kesalahan.

Ada juga ShellOut , yang juga mendukung berbagai perintah yang telah ditentukan sebelumnya.

Baleb
sumber