Как запустить команду терминала в сценарии Swift? (например, xcodebuild)


87

Я хочу заменить свои сценарии CI bash на swift. Я не могу понять, как вызвать обычную команду терминала, такую ​​как lsилиxcodebuild

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

Ответы:


136

Если вы не используете выходные данные команд в коде Swift, достаточно будет следующего:

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

Обновлено: для Swift3 / Xcode8


3
«NSTask» был переименован в «Процесс»
Матеуш

4
Process () все еще в Swift 4? Я получаю неопределенный символ. : /
Арнальдо Капо

1
@ArnaldoCapo У меня все еще отлично работает! Вот пример:#!/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

2
Я попробовал, что у меня получилось: я попробовал, что получил: i.imgur.com/Ge1OOCG.png
cyber8200

4
Процесс доступен только на macOS
shallowThought

86

Если вы хотите использовать аргументы командной строки «точно», как в командной строке (без разделения всех аргументов), попробуйте следующее.

(Этот ответ лучше ответа LegoLess и может быть использован в 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")

6
Этот ответ действительно должен быть намного выше, поскольку он решает многие проблемы предыдущих.
Стивен Хептинг,

1
+1. Следует отметить для пользователей osx, что /bin/bashссылка на bash-3.2. Если вы хотите использовать более продвинутые функции bash, измените путь ( /usr/bin/env bashобычно это хорошая альтернатива)
Aserre

Кто-нибудь может с этим помочь? Аргументы не проходят stackoverflow.com/questions/62203978/…
Махди

34

Проблема здесь в том, что вы не можете смешивать и сопоставлять Bash и Swift. Вы уже знаете, как запускать скрипт Swift из командной строки, теперь вам нужно добавить методы для выполнения команд Shell в Swift. В итоге из блога 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
}

Следующий код Swift будет выполняться xcodebuildс аргументами, а затем выводить результат.

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

Что касается поиска в содержимом каталога (что и lsпроисходит в Bash), я предлагаю использовать NSFileManagerи сканировать каталог непосредственно в Swift, а не в выводе Bash, что может быть затруднительно для анализа.


1
Отлично - я внес несколько изменений, чтобы сделать эту компиляцию, однако я получаю исключение во время выполнения при попытке вызвать shell("ls", [])- 'NSInvalidArgumentException', reason: 'launch path not accessible' Есть идеи?
Роберт

5
NSTask не выполняет поиск в исполняемом файле (используя ваш PATH из среды), как это делает оболочка. Путь запуска должен быть абсолютным (например, «/ bin / ls») или путем относительно текущего рабочего каталога.
Martin R

stackoverflow.com/questions/386783/… PATH - это в основном концепция оболочки и недостижима.
Legoless

Отлично - теперь работает. Выложил полный сценарий + несколько модификаций для полноты картины. Спасибо.
Роберт

2
Используя оболочку ("cd", "~ / Desktop /"), я получаю: / usr / bin / cd: line 4: cd: ~ / Desktop /: Нет такого файла или каталога
Запорожченко Александр

21

Служебная функция в Swift 3.0

Это также возвращает статус завершения задачи и ожидает завершения.

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 Foundationпропавший без вести
Binarian 05

3
К сожалению, не для iOS.
Рафаэль

16

Если вы хотите использовать среду bash для вызова команд, используйте следующую функцию bash, которая использует исправленную версию Legoless. Мне пришлось удалить завершающую новую строку из результата функции оболочки.

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

Например, чтобы получить текущую рабочую ветку git текущего рабочего каталога:

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

12

Полный сценарий на основе ответа 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"])

10

Просто чтобы обновить это, поскольку Apple устарела как .launchPath, так и launch (), вот обновленная служебная функция для Swift 4, которая должна быть немного более надежной в будущем.

Примечание: документация Apple по заменам ( run () , исполняемый URL и т. Д.) В основном пуста.

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

Должен быть в состоянии вставить это прямо на игровую площадку, чтобы увидеть его в действии.


8

Обновление для Swift 4.0 (касается изменений 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)
}

приведу пример
Гаутам Сорьярадж

3

Попробовав некоторые из размещенных здесь решений, я обнаружил, что лучший способ выполнять команды - использовать -cфлаг для аргументов.

@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

Смешивание ответов Ринтаро и Legoless для 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

Небольшое улучшение с поддержкой переменных 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

Пример использования класса Process для запуска скрипта Python.

Также:

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

Куда вы поместите файл "upload.py"
Сухайб Роуми

0

Я создал SwiftExec , небольшую библиотеку для выполнения таких команд:

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

Это однофайловая библиотека, которую можно легко скопировать в проекты или установить с помощью SPM. Он протестирован и упрощает обработку ошибок.

Также есть ShellOut , который дополнительно поддерживает множество предопределенных команд.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.