Como executo um comando de terminal em um script Swift? (por exemplo, xcodebuild)

87

Quero substituir meus scripts de bash de CI por swift. Não consigo descobrir como invocar um comando de terminal normal, como 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
fonte

Respostas:

136

Se você não usar saídas de comando no código Swift, o seguinte seria suficiente:

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

Atualizado: para Swift3 / Xcode8

Rintaro
fonte
3
'NSTask' foi renomeado para 'Processo'
Mateusz
4
O processo () ainda está no Swift 4? Estou recebendo um símbolo indefinido. : /
Arnaldo Capo
1
@ArnaldoCapo Ainda funciona bem para mim! Aqui está um exemplo:#!/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 05 de
2
Tentei que consegui: Tentei que consegui: i.imgur.com/Ge1OOCG.png
cyber8200
4
O processo está disponível apenas no macOS
shallowThought
85

Se você gostaria de usar argumentos de linha de comando "exatamente" como faria na linha de comando (sem separar todos os argumentos), tente o seguinte.

(Esta resposta melhora a resposta da LegoLess e pode ser usada no 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
fonte
6
Essa resposta deve ser muito mais elevada, pois resolve muitos dos problemas das anteriores.
Steven Hepting
1
+1. Deve ser observado para usuários de osx que /bin/bashse refere a bash-3.2. Se você quiser usar os recursos mais avançados do bash, altere o caminho ( /usr/bin/env bashgeralmente é uma boa alternativa)
Aserre
Alguém pode ajudar com isso? Argumentos não passam stackoverflow.com/questions/62203978/…
mahdi
34

O problema aqui é que você não pode misturar e combinar Bash e Swift. Você já sabe como executar o script Swift na linha de comando, agora você precisa adicionar os métodos para executar comandos do Shell no Swift. Em resumo do 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
}

O código Swift a seguir será executado xcodebuildcom argumentos e, em seguida, produzirá o resultado.

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

Quanto à pesquisa do conteúdo do diretório (que é o que lsfaz no Bash), sugiro usar NSFileManagere escanear o diretório diretamente no Swift, em vez da saída do Bash, o que pode ser difícil de analisar.

Sem legol
fonte
1
Ótimo - fiz algumas edições para fazer essa compilação, mas estou recebendo uma exceção de tempo de execução ao tentar invocar shell("ls", [])- 'NSInvalidArgumentException', reason: 'launch path not accessible' Alguma ideia?
Robert
5
NSTask não pesquisa o executável (usando seu PATH do ambiente) como o shell faz. O caminho de inicialização deve ser um caminho absoluto (por exemplo, "/ bin / ls") ou um caminho relativo ao diretório de trabalho atual.
Martin R
stackoverflow.com/questions/386783/… PATH é basicamente um conceito de shell e não pode ser alcançado.
Legoless de
Ótimo - agora funciona. Publiquei o script completo + algumas modificações para ser completo. Obrigado.
Robert
2
Usando o shell ("cd", "~ / Desktop /"), obtenho: / usr / bin / cd: linha 4: cd: ~ / Desktop /: Não existe esse arquivo ou diretório
Zaporozhchenko Oleksandr
21

Função de utilidade em Swift 3.0

Isso também retorna o status de encerramento das tarefas e aguarda a conclusão.

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)
}
Uma corrida
fonte
5
import Foundationausente
Binarian
3
Infelizmente, não para iOS.
Raphael
16

Se você gostaria de usar o ambiente bash para chamar comandos, use a seguinte função bash que usa uma versão corrigida do Legoless. Tive que remover uma nova linha final do resultado da função do 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)
}

Por exemplo, para obter o branch git de trabalho atual do diretório de trabalho atual:

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

Roteiro completo baseado na resposta 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
fonte
10

Apenas para atualizar isso, já que a Apple tornou .launchPath e launch () obsoletos, aqui está uma função de utilitário atualizada para Swift 4 que deve ser um pouco mais preparada para o futuro.

Observação: a documentação da Apple sobre as substituições ( run () , executableURL , etc) está basicamente vazia neste ponto.

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

Deve ser capaz de colar isso diretamente em um playground para vê-lo em ação.

angusc
fonte
8

Atualizando para Swift 4.0 (lidando com mudanças em 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
fonte
dê o exemplo
Gowtham Sooryaraj
3

Depois de tentar algumas das soluções postadas aqui, descobri que a melhor maneira de executar comandos era usando o -csinalizador para os argumentos.

@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
fonte
0

Misturando as respostas de Rintaro e Legoless para 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
}
rico
fonte
0

Pequena melhoria com o suporte para variáveis ​​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)
}
Alexander Belyavskiy
fonte
0

Exemplo de uso da classe Process para executar um script Python.

Além disso:

 - 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
fonte
onde você coloca o arquivo "upload.py '
Suhaib Roomy
0

Eu construí SwiftExec , uma pequena biblioteca para a execução de tais comandos:

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

É uma biblioteca de arquivo único que pode ser facilmente copiada e colada em projetos ou instalada usando SPM. É testado e simplifica o tratamento de erros.

Há também ShellOut , que também oferece suporte a uma variedade de comandos predefinidos.

Baleb
fonte