Wie führe ich einen Terminalbefehl in einem schnellen Skript aus? (zB xcodebuild)


87

Ich möchte meine CI-Bash-Skripte durch Swift ersetzen. Ich kann nicht herausfinden, wie man einen normalen Terminalbefehl wie lsoder aufruftxcodebuild

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

Antworten:


136

Wenn Sie im Swift-Code keine Befehlsausgaben verwenden, ist Folgendes ausreichend:

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

Aktualisiert: für Swift3 / Xcode8


3
'NSTask' wurde in 'Process' umbenannt
Mateusz

4
Ist Process () noch in Swift 4? Ich bekomme ein undefiniertes Symbol. : /
Arnaldo Capo

1
@ArnaldoCapo Es funktioniert immer noch gut für mich! Hier ist ein Beispiel:#!/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
Ich habe versucht, dass ich bekam: Ich habe versucht, dass ich bekam: i.imgur.com/Ge1OOCG.png
cyber8200

4
Prozess ist nur unter macOS verfügbar
flatThought

85

Wenn Sie Befehlszeilenargumente "genau" wie in der Befehlszeile verwenden möchten (ohne alle Argumente zu trennen), versuchen Sie Folgendes.

(Diese Antwort verbessert die Antwort von LegoLess und kann in Swift 5 verwendet werden.)

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

6
Diese Antwort sollte wirklich viel höher sein, da sie viele der Probleme der vorherigen löst.
Steven Hepting

1
+1. Es sollte für osx Benutzer darauf hingewiesen , dass /bin/bashbezieht bash-3.2. Wenn Sie die erweiterten Funktionen von bash verwenden möchten, ändern Sie den Pfad ( /usr/bin/env bashnormalerweise eine gute Alternative)
Aserre

Kann jemand dabei helfen? Argumente bestehen nicht auf stackoverflow.com/questions/62203978/…
mahdi

34

Das Problem hierbei ist, dass Sie Bash und Swift nicht kombinieren können. Sie wissen bereits, wie das Swift-Skript über die Befehlszeile ausgeführt wird. Jetzt müssen Sie die Methoden zum Ausführen von Shell-Befehlen in Swift hinzufügen. Zusammenfassend aus dem PracticalSwift- Blog:

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
}

Der folgende Swift-Code wird xcodebuildmit Argumenten ausgeführt und gibt dann das Ergebnis aus.

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

Was das Durchsuchen des Verzeichnisinhalts lsbetrifft (was in Bash der Fall ist), empfehle ich, NSFileManagerdas Verzeichnis direkt in Swift zu verwenden und zu scannen, anstatt die Bash-Ausgabe, was schwierig zu analysieren sein kann.


1
Groß - Ich habe ein paar Änderungen diese Kompilierung zu machen, aber ich bin eine Laufzeitausnahme bekommen , wenn aufzurufen versuchen shell("ls", [])- 'NSInvalidArgumentException', reason: 'launch path not accessible' Irgendwelche Ideen?
Robert

5
NSTask durchsucht die ausführbare Datei nicht (unter Verwendung Ihres PATH aus der Umgebung) wie die Shell. Der Startpfad muss ein absoluter Pfad (z. B. "/ bin / ls") oder ein Pfad relativ zum aktuellen Arbeitsverzeichnis sein.
Martin R

stackoverflow.com/questions/386783/… PATH ist im Grunde ein Shell-Konzept und nicht erreichbar.
Legoless

Großartig - es funktioniert jetzt. Der Vollständigkeit halber habe ich das vollständige Skript und einige Änderungen veröffentlicht. Vielen Dank.
Robert

2
Unter Verwendung der Shell ("cd", "~ / Desktop /") erhalte ich: / usr / bin / cd: Zeile 4: cd: ~ / Desktop /: Keine solche Datei oder kein solches Verzeichnis
Zaporozhchenko Oleksandr

21

Dienstprogrammfunktion In Swift 3.0

Dies gibt auch den Status der Aufgabenbeendigung zurück und wartet auf den Abschluss.

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 Foundationfehlt
Binarian

3
Leider nicht für iOS.
Raphael

16

Wenn Sie die Bash-Umgebung zum Aufrufen von Befehlen verwenden möchten, verwenden Sie die folgende Bash-Funktion, die eine feste Version von Legoless verwendet. Ich musste einen nachgestellten Zeilenumbruch aus dem Ergebnis der Shell-Funktion entfernen.

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

Zum Beispiel, um den aktuellen Arbeits-Git-Zweig des aktuellen Arbeitsverzeichnisses abzurufen:

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

12

Vollständiges Skript basierend auf Legoless 'Antwort

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

Um dies zu aktualisieren, da Apple sowohl .launchPath als auch launch () nicht mehr unterstützt, finden Sie hier eine aktualisierte Dienstprogrammfunktion für Swift 4, die etwas zukunftssicherer sein sollte.

Hinweis: Die Dokumentation von Apple zu den Ersetzungen ( run () , ausführbare URL usw.) ist zu diesem Zeitpunkt grundsätzlich leer.

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

Sollte in der Lage sein, dies direkt in einen Spielplatz einzufügen, um es in Aktion zu sehen.


8

Aktualisierung für Swift 4.0 (Änderungen an 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)
}

Geben Sie das Beispiel
Gowtham Sooryaraj

3

Nachdem ich einige der hier veröffentlichten Lösungen ausprobiert hatte, stellte ich fest, dass der beste Weg zum Ausführen von Befehlen darin bestand, das -cFlag für die Argumente zu verwenden.

@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

Mischen von Rintaro und Legoless 'Antworten für 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

Kleine Verbesserung mit der Unterstützung von env-Variablen:

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

Beispiel für die Verwendung der Process-Klasse zum Ausführen eines Python-Skripts.

Ebenfalls:

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

Wo platzieren Sie die Datei "upload.py"
Suhaib Roomy

0

Ich habe SwiftExec erstellt , eine kleine Bibliothek zum Ausführen solcher Befehle:

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

Es handelt sich um eine Einzeldateibibliothek, die einfach in Projekte kopiert oder mit SPM installiert werden kann. Es wurde getestet und vereinfacht die Fehlerbehandlung.

Es gibt auch ShellOut , das zusätzlich eine Vielzahl vordefinierter Befehle unterstützt.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.