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


87

Я хочу замінити свої сценарії Cash bash на швидкі. Я не можу зрозуміти, як викликати звичайні команди терміналу, такі як 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' було перейменовано на 'Process'
Матеуш,

4
Процес () все ще знаходиться в 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

2
Я спробував, що отримав: Спробував, що отримав: i.imgur.com/Ge1OOCG.png
cyber8200

4
Процес доступний лише на macOS
неглибоко,

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зниклий
Бінаріан

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 () , executableURL тощо) на даний момент порожня.

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

наведемо приклад
Gowtham Sooryaraj

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

Поєднання відповідей rintaro та 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.