Comment exécuter une commande de terminal dans un script Swift? (par exemple xcodebuild)

87

Je souhaite remplacer mes scripts CI bash par swift. Je ne peux pas comprendre comment appeler une commande de terminal normale telle que lsouxcodebuild

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

Réponses:

136

Si vous n'utilisez pas les sorties de commande en code Swift, ce qui suit serait suffisant:

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

Mise à jour: pour Swift3 / Xcode8

Rintaro
la source
3
'NSTask' a été renommé en 'Process'
Mateusz
4
Process () est-il toujours dans Swift 4? J'obtiens un symbole indéfini. : /
Arnaldo Capo
1
@ArnaldoCapo Cela fonctionne toujours bien pour moi! Voici un exemple:#!/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
J'ai essayé ce que j'ai obtenu: j'ai essayé que j'ai obtenu: i.imgur.com/Ge1OOCG.png
cyber8200
4
Le processus est disponible sur macOS uniquement
shallowThought
85

Si vous souhaitez utiliser des arguments de ligne de commande "exactement" comme vous le feriez en ligne de commande (sans séparer tous les arguments), essayez ce qui suit.

(Cette réponse améliore la réponse de LegoLess et peut être utilisée dans 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/bash"
    task.launch()

    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output = String(data: data, encoding: .utf8)!

    return output
}

// Example usage:
shell("ls -la")
user3064009
la source
6
Cette réponse devrait vraiment être beaucoup plus élevée car elle résout de nombreux problèmes des précédents.
Steven Hepting
1
+1. Il convient de noter pour les utilisateurs osx qui font /bin/bashréférence à bash-3.2. Si vous souhaitez utiliser les fonctionnalités plus avancées de bash, changez le chemin (c'est /usr/bin/env bashgénéralement une bonne alternative)
Aserre
Quelqu'un peut-il vous aider? Les arguments ne passent pas stackoverflow.com/questions/62203978/…
mahdi
34

Le problème ici est que vous ne pouvez pas mélanger et assortir Bash et Swift. Vous savez déjà comment exécuter le script Swift à partir de la ligne de commande, vous devez maintenant ajouter les méthodes pour exécuter les commandes Shell dans Swift. En résumé du 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
}

Le code Swift suivant s'exécutera xcodebuildavec des arguments, puis affichera le résultat.

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

En ce qui concerne la recherche du contenu du répertoire (ce qui est lsfait dans Bash), je suggère d'utiliser NSFileManageret de scanner le répertoire directement dans Swift, au lieu de la sortie Bash, ce qui peut être difficile à analyser.

Legoless
la source
1
Génial - J'ai fait quelques modifications pour faire cette compilation, mais j'obtiens une exception d'exécution en essayant d'appeler shell("ls", [])- 'NSInvalidArgumentException', reason: 'launch path not accessible' Des idées?
Robert
5
NSTask ne recherche pas l'exécutable (en utilisant votre PATH de l'environnement) comme le fait le shell. Le chemin de lancement doit être un chemin absolu (par exemple "/ bin / ls") ou un chemin relatif au répertoire de travail courant.
Martin R
stackoverflow.com/questions/386783/... PATH est fondamentalement le concept d'un shell et n'est pas accessible.
Legoless
Génial - cela fonctionne maintenant. J'ai posté le script complet + quelques modifications pour être complet. Merci.
Robert
2
En utilisant le shell ("cd", "~ / Desktop /"), j'obtiens: / usr / bin / cd: line 4: cd: ~ / Desktop /: Aucun fichier ou répertoire de ce type
Zaporozhchenko Oleksandr
21

Fonction utilitaire dans Swift 3.0

Cela renvoie également l'état de fin des tâches et attend la fin.

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
la source
5
import Foundationmissing
Binarian
3
Malheureusement, pas pour iOS.
Raphael
16

Si vous souhaitez utiliser l'environnement bash pour appeler des commandes, utilisez la fonction bash suivante qui utilise une version corrigée de Legoless. J'ai dû supprimer une nouvelle ligne du résultat de la fonction 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)
}

Par exemple, pour obtenir la branche git de travail actuelle du répertoire de travail actuel:

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

Script complet basé sur la réponse de 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
la source
10

Juste pour mettre à jour cela puisque Apple a désapprouvé à la fois .launchPath et launch (), voici une fonction utilitaire mise à jour pour Swift 4 qui devrait être un peu plus à l'épreuve du temps.

Remarque: la documentation d'Apple sur les remplacements ( run () , executableURL , etc.) est pratiquement vide à ce stade.

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

Devrait pouvoir le coller directement dans une aire de jeux pour le voir en action.

angusc
la source
8

Mise à jour pour Swift 4.0 (gestion des modifications apportées à 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
la source
donner l'exemple
Gowtham Sooryaraj
3

Après avoir essayé certaines des solutions publiées ici, j'ai trouvé que la meilleure façon d'exécuter des commandes était d'utiliser l' -cindicateur pour les arguments.

@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
la source
0

Mélanger les réponses de Rintaro et Legoless pour 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
la source
0

Petite amélioration avec le support des variables 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)
}
Alexandre Belyavskiy
la source
0

Exemple d'utilisation de la classe Process pour exécuter un script Python.

Aussi:

 - 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
la source
où placez-vous le fichier «upload.py»
Suhaib Roomy
0

J'ai construit SwiftExec , une petite bibliothèque pour exécuter de telles commandes:

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

C'est une bibliothèque à un seul fichier qui peut facilement être copiée-collée dans des projets ou installée à l'aide de SPM. Il est testé et simplifie la gestion des erreurs.

Il existe également ShellOut , qui prend également en charge une variété de commandes prédéfinies.

Baleb
la source