ฉันจะเรียกใช้คำสั่งเทอร์มินัลในสคริปต์ Swift ได้อย่างไร (เช่น xcodebuild)


91

ฉันต้องการแทนที่สคริปต์ทุบตี CI ของฉันด้วยความรวดเร็ว ฉันไม่สามารถหาวิธีเรียกใช้คำสั่งเทอร์มินัลปกติเช่น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 ....

คำตอบ:


138

หากคุณไม่ได้ใช้เอาต์พุตคำสั่งในโค้ด 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


3
'NSTask' ถูกเปลี่ยนชื่อเป็น 'Process'
Mateusz

4
Process () ยังอยู่ใน Swift 4 หรือไม่ ฉันได้รับสัญลักษณ์ที่ไม่ได้กำหนด : /
Arnaldo Capo

1
@ArnaldoCapo มันยังใช้ได้ดีสำหรับฉัน! นี่คือตัวอย่าง:#!/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
ฉันพยายามแล้วฉันได้: ฉันพยายามแล้วฉันได้: i.imgur.com/Ge1OOCG.png
cyber8200

4
กระบวนการใช้งานได้บน macOS เท่านั้น
ตื้นคิด

93

หากคุณต้องการใช้อาร์กิวเมนต์บรรทัดคำสั่ง "ตรงทั้งหมด" เหมือนที่คุณทำในบรรทัดคำสั่ง (โดยไม่แยกอาร์กิวเมนต์ทั้งหมด) ให้ลองทำดังต่อไปนี้

(คำตอบนี้ปรับปรุงจากคำตอบของ LegoLess และสามารถใช้ได้ใน 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")

7
คำตอบนี้ควรจะสูงกว่านี้มากเนื่องจากสามารถแก้ปัญหาต่างๆของข้อก่อนหน้าได้
Steven Hepting

1
+1. มันควรจะตั้งข้อสังเกตสำหรับผู้ใช้ OSX ว่าหมายถึง/bin/bash bash-3.2หากคุณต้องการใช้คุณสมบัติขั้นสูงของ bash ให้เปลี่ยนเส้นทาง ( /usr/bin/env bashโดยปกติจะเป็นทางเลือกที่ดี)
Aserre

ใครสามารถช่วยเรื่องนี้? อาร์กิวเมนต์ไม่ผ่านstackoverflow.com/questions/62203978/…
mahdi

34

ปัญหาคือคุณไม่สามารถผสมและจับคู่ Bash และ Swift ได้ คุณรู้วิธีเรียกใช้สคริปต์ Swift จากบรรทัดคำสั่งแล้วตอนนี้คุณต้องเพิ่มวิธีการเพื่อดำเนินการคำสั่งเชลล์ใน 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 output ซึ่งอาจเป็นความเจ็บปวดในการแยกวิเคราะห์


1
เยี่ยมมาก - ฉันได้ทำการแก้ไขเล็กน้อยเพื่อสร้างคอมไพล์นี้ แต่ฉันได้รับข้อยกเว้นรันไทม์เมื่อพยายามเรียกใช้shell("ls", [])- มี'NSInvalidArgumentException', reason: 'launch path not accessible' ความคิดใดบ้าง?
Robert

5
NSTask ไม่ค้นหาไฟล์ปฏิบัติการ (โดยใช้ PATH ของคุณจากสภาพแวดล้อม) เหมือนที่เชลล์ทำ เส้นทางการเรียกใช้งานต้องเป็นเส้นทางที่แน่นอน (เช่น "/ bin / ls") หรือเส้นทางที่สัมพันธ์กับไดเร็กทอรีการทำงานปัจจุบัน
Martin R

stackoverflow.com/questions/386783/… PATH เป็นแนวคิดของเชลล์และไม่สามารถเข้าถึงได้
Legoless

เยี่ยมมาก - ใช้งานได้แล้ว ฉันโพสต์สคริปต์ฉบับเต็ม + การแก้ไขเล็กน้อยเพื่อความสมบูรณ์ ขอขอบคุณ.
Robert

2
เมื่อใช้ shell ("cd", "~ / Desktop /") ฉันจะได้รับ: / usr / bin / cd: line 4: cd: ~ / Desktop /: ไม่มีไฟล์หรือไดเร็กทอรีดังกล่าว
Zaporozhchenko Oleksandr

22

ฟังก์ชันยูทิลิตี้ใน 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)
}

5
import Foundationหายไป
Binarian

3
น่าเศร้าที่ไม่ใช่สำหรับ iOS
Raphael

17

หากคุณต้องการใช้สภาพแวดล้อม bash สำหรับการเรียกคำสั่งให้ใช้ฟังก์ชัน bash ต่อไปนี้ซึ่งใช้ Legoless เวอร์ชันที่แก้ไขแล้ว ฉันต้องลบบรรทัดใหม่ต่อท้ายออกจากผลลัพธ์ของฟังก์ชันเชลล์

สวิฟต์ 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)")

13

สคริปต์เต็มตามคำตอบของ 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"])

11

เพียงแค่อัปเดตสิ่งนี้เนื่องจาก Apple ได้เลิกใช้ทั้ง. lunchPath และ 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")

ควรวางสิ่งนี้ลงในสนามเด็กเล่นโดยตรงเพื่อดูการทำงานจริง


8

การอัปเดตสำหรับ 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)
}

ยกตัวอย่าง
Gowtham Sooryaraj

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

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
}

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

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

คุณวางไฟล์ "upload.py" ไว้
ที่ไหน

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ซึ่งรองรับคำสั่งที่กำหนดไว้ล่วงหน้าอีกมากมาย

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.