Come posso eseguire un comando da terminale in uno script Swift? (ad es. xcodebuild)


91

Voglio sostituire i miei script bash CI con swift. Non riesco a capire come invocare un normale comando da terminale come lsoxcodebuild

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

Risposte:


138

Se non utilizzi gli output dei comandi nel codice Swift, sarebbe sufficiente quanto segue:

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

Aggiornato: per Swift3 / Xcode8


3
"NSTask" è stato rinominato "Process"
Mateusz

4
Process () è ancora in Swift 4? Ricevo un simbolo indefinito. : /
Arnaldo Capo

1
@ArnaldoCapo Funziona ancora bene per me! Ecco un esempio:#!/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
Ho provato che ho ottenuto: Ho provato che ho ottenuto: i.imgur.com/Ge1OOCG.png
cyber8200

4
Il processo è disponibile solo su macOS
shallowThought

93

Se desideri utilizzare gli argomenti della riga di comando "esattamente" come faresti nella riga di comando (senza separare tutti gli argomenti), prova quanto segue.

(Questa risposta migliora rispetto alla risposta di LegoLess e può essere utilizzata in 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
Questa risposta dovrebbe davvero essere molto più in alto in quanto risolve molti dei problemi dei precedenti.
Steven Hepting

1
+1. Va notato per gli utenti OSX che si /bin/bashriferiscono a bash-3.2. Se vuoi utilizzare le funzionalità più avanzate di bash, cambia il percorso (di /usr/bin/env bashsolito è una buona alternativa)
Aserre

Qualcuno può aiutare con questo? Gli argomenti non superano stackoverflow.com/questions/62203978/…
mahdi

34

Il problema qui è che non puoi mescolare e abbinare Bash e Swift. Sai già come eseguire Swift script dalla riga di comando, ora devi aggiungere i metodi per eseguire i comandi Shell in Swift. In sintesi dal 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
}

Il seguente codice Swift verrà eseguito xcodebuildcon argomenti e quindi produrrà il risultato.

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

Per quanto riguarda la ricerca nel contenuto della directory (che è ciò che lsfa in Bash), suggerisco di utilizzare NSFileManagere scansionare la directory direttamente in Swift, invece dell'output di Bash, che può essere un problema da analizzare.


1
Ottimo - Ho apportato alcune modifiche per rendere questa compilazione, tuttavia ricevo un'eccezione di runtime quando provo a richiamare shell("ls", [])- 'NSInvalidArgumentException', reason: 'launch path not accessible' Qualche idea?
Robert,

5
NSTask non cerca nell'eseguibile (utilizzando il PATH dall'ambiente) come fa la shell. Il percorso di avvio deve essere un percorso assoluto (ad esempio "/ bin / ls") o un percorso relativo alla directory di lavoro corrente.
Martin R

stackoverflow.com/questions/386783/… PATH è fondamentalmente un concetto di shell e non è raggiungibile.
Legoless

Fantastico, ora funziona. Ho pubblicato lo script completo + alcune modifiche per completezza. Grazie.
Robert

2
Usando la shell ("cd", "~ / Desktop /"), ottengo: / usr / bin / cd: riga 4: cd: ~ / Desktop /: Nessun file o directory di questo tipo
Zaporozhchenko Oleksandr

22

Funzione di utilità In Swift 3.0

Ciò restituisce anche lo stato di conclusione delle attività e attende il completamento.

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

3
Purtroppo, non per iOS.
Raphael

17

Se desideri utilizzare l'ambiente bash per chiamare i comandi, utilizza la seguente funzione bash che utilizza una versione fissa di Legoless. Ho dovuto rimuovere una nuova riga finale dal risultato della funzione di 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)
}

Ad esempio, per ottenere il ramo git di lavoro corrente della directory di lavoro corrente:

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

13

Sceneggiatura completa basata sulla risposta di 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

Solo per aggiornarlo poiché Apple ha deprecato sia .launchPath che launch (), ecco una funzione di utilità aggiornata per Swift 4 che dovrebbe essere un po 'più a prova di futuro.

Nota: la documentazione di Apple sulle sostituzioni ( run () , executableURL , ecc.) A questo punto è praticamente vuota.

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

Dovresti essere in grado di incollarlo direttamente in un parco giochi per vederlo in azione.


8

Aggiornamento per Swift 4.0 (gestione delle modifiche a 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)
}

fai

4

Dopo aver provato alcune delle soluzioni pubblicate qui, ho scoperto che il modo migliore per eseguire i comandi era usare il -cflag per gli argomenti.

@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

Mescolando le risposte di Rintaro e Legoless per 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

Piccolo miglioramento con il supporto per le variabili 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

Esempio di utilizzo della classe Process per eseguire uno script Python.

Anche:

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

dove metti il ​​file "upload.py '
Suhaib Roomy

0

Ho creato SwiftExec , una piccola libreria per eseguire tali comandi:

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

È una libreria a file singolo che può essere facilmente copiata e incollata nei progetti o installata utilizzando SPM. È testato e semplifica la gestione degli errori.

C'è anche ShellOut , che supporta inoltre una varietà di comandi predefiniti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.