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 ....
Robert
quelle

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

Rintaro
quelle
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")
user3064009
quelle
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.

Legoless
quelle
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)
}
Arun
quelle
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)")
Pellet
quelle
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"])
Robert
quelle
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.

angusc
quelle
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)
}
rougeExciter
quelle
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")
Lojals
quelle
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
}
reich
quelle
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)
}
Alexander Belyavskiy
quelle
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)")
   }
}
Janusz Chudzynski
quelle
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.

Baleb
quelle