Jak uruchomić polecenie terminala w skrypcie Swift? (np. xcodebuild)

91

Chcę zamienić moje skrypty bash CI na szybkie. Nie mogę dowiedzieć się, jak wywołać normalne polecenie terminala, takie jak lslubxcodebuild

#!/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
źródło

Odpowiedzi:

138

Jeśli nie używasz danych wyjściowych poleceń w kodzie Swift, wystarczy:

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

Zaktualizowano: dla Swift3 / Xcode8

rintaro
źródło
3
„NSTask” został przemianowany na „Process”
Mateusz
4
Czy Process () nadal znajduje się w Swift 4? Otrzymuję niezdefiniowany symbol. : /
Arnaldo Capo
1
@ArnaldoCapo To nadal działa dobrze dla mnie! Oto przykład:#!/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
Próbowałem, że dostałem: próbowałem, że dostałem: i.imgur.com/Ge1OOCG.png
cyber8200
5
Proces jest dostępny tylko w systemie macOS
shallowThought
94

Jeśli chcesz używać argumentów wiersza poleceń „dokładnie” tak, jak w wierszu poleceń (bez oddzielania wszystkich argumentów), wypróbuj następujące rozwiązania.

(Ta odpowiedź jest poprawą odpowiedzi LegoLess i może być używana w 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")
user3064009
źródło
7
Ta odpowiedź naprawdę powinna być znacznie wyższa, ponieważ rozwiązuje wiele problemów poprzednich.
Steven Hepting
1
+1. Należy zauważyć, że użytkownicy osx /bin/bashodnoszą się do bash-3.2. Jeśli chcesz korzystać z bardziej zaawansowanych funkcji basha, zmień ścieżkę ( /usr/bin/env bashzwykle jest to dobra alternatywa)
Aserre
Czy ktoś może w tym pomóc? Argumenty nie przechodzą przez stackoverflow.com/questions/62203978/ ...
mahdi
34

Problem polega na tym, że nie możesz mieszać i dopasowywać Bash i Swift. Wiesz już, jak uruchomić skrypt Swift z wiersza poleceń, teraz musisz dodać metody do wykonywania poleceń powłoki w języku Swift. Podsumowując z bloga 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
}

Poniższy kod Swift zostanie wykonany xcodebuildz argumentami, a następnie wyprowadzi wynik.

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

Jeśli chodzi o przeszukiwanie zawartości katalogu (co lsrobi w Bash), sugeruję używanie NSFileManageri skanowanie katalogu bezpośrednio w Swift, zamiast wyjścia Bash, co może być trudne do przeanalizowania.

Bezprawia
źródło
1
Świetnie - wprowadziłem kilka zmian, aby skompilować tę kompilację, jednak podczas próby wywołania pojawia się wyjątek środowiska wykonawczego shell("ls", [])- 'NSInvalidArgumentException', reason: 'launch path not accessible' jakieś pomysły?
Robert
5
NSTask nie przeszukuje pliku wykonywalnego (używając twojej PATH ze środowiska) tak jak robi to powłoka. Ścieżka uruchamiania musi być ścieżką bezwzględną (np. „/ Bin / ls”) lub ścieżką względną do bieżącego katalogu roboczego.
Martin R
stackoverflow.com/questions/386783/… PATH jest zasadniczo koncepcją powłoki i nieosiągalna.
Bezprawny
Świetnie - teraz działa. Opublikowałem pełny skrypt + kilka modyfikacji dla kompletności. Dziękuję Ci.
Robert
2
Używając powłoki ("cd", "~ / Desktop /"), otrzymuję: / usr / bin / cd: wiersz 4: cd: ~ / Desktop /: Brak takiego pliku lub katalogu
Zaporożczenko Oleksandr
23

Funkcja użyteczności w Swift 3.0

Powoduje to również zwrócenie stanu zakończenia zadań i oczekuje na zakończenie.

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)
}
Bieg
źródło
5
import Foundationzaginiony
Binarian
3
Niestety nie na iOS.
Raphael
17

Jeśli chcesz używać środowiska bash do wywoływania poleceń, użyj następującej funkcji bash, która używa poprawionej wersji Legoless. Musiałem usunąć kończący znak nowej linii z wyniku funkcji powłoki.

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

Na przykład, aby uzyskać bieżącą działającą gałąź git bieżącego katalogu roboczego:

let currentBranch = bash("git", arguments: ["describe", "--contains", "--all", "HEAD"])
print("current branch:\(currentBranch)")
Śrut
źródło
13

Pełny scenariusz oparty na odpowiedzi 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
źródło
11

Aby to zaktualizować, ponieważ Apple wycofał zarówno .launchPath, jak i launch (), oto zaktualizowana funkcja narzędzia dla Swift 4, która powinna być nieco bardziej odporna na przyszłość.

Uwaga: dokumentacja Apple dotycząca zamienników ( run () , executableURL itp.) Jest w zasadzie pusta w tym momencie.

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

Powinien móc wkleić to bezpośrednio do placu zabaw, aby zobaczyć, jak działa.

angusc
źródło
8

Aktualizacja do Swift 4.0 (radzenie sobie ze zmianami w 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
źródło
podaj przykład
Gowtham Sooryaraj
4

Po wypróbowaniu niektórych z zamieszczonych tutaj rozwiązań stwierdziłem, że najlepszym sposobem wykonywania poleceń jest użycie -cflagi dla argumentów.

@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
źródło
0

Mieszanie odpowiedzi rintaro i Legoless dla 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
}
Richy
źródło
0

Mała poprawa z obsługą zmiennych 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
źródło
0

Przykład użycia klasy Process do uruchomienia skryptu w Pythonie.

Również:

 - 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 Chudzyński
źródło
gdzie umieszczasz plik "upload.py"
Suhaib Roomy
0

Zbudowałem SwiftExec , małą bibliotekę do uruchamiania takich poleceń:

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

Jest to biblioteka jednoplikowa, którą można łatwo skopiować i wkleić do projektów lub zainstalować za pomocą SPM. Jest przetestowany i upraszcza obsługę błędów.

Dostępny jest również ShellOut , który dodatkowo obsługuje różne predefiniowane polecenia.

Baleb
źródło