programing

Swift 스크립트에서 터미널 명령을 실행하려면 어떻게 해야 합니까?(예: xcodebuild)입니다.

yellowcard 2023. 4. 25. 22:15
반응형

Swift 스크립트에서 터미널 명령을 실행하려면 어떻게 해야 합니까?(예: xcodebuild)입니다.

CI 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 ....

명령줄 인수를 모든 인수를 분리하지 않고 "정확하게" 사용하려면 다음을 수행하십시오.

(이 답변은 LegoLess의 답변보다 개선되었으며 Swift 5에서 사용할 수 있습니다.)

import Foundation

func shell(_ command: String) -> String {
    let task = Process()
    let pipe = Pipe()
    
    task.standardOutput = pipe
    task.standardError = pipe
    task.arguments = ["-c", command]
    task.launchPath = "/bin/zsh"
    task.standardInput = nil
    task.launch()
    
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output = String(data: data, encoding: .utf8)!
    
    return output
}

// Example usage:
shell("ls -la")

업데이트됨/안전한 함수 호출 10/23/21: 위의 셸 명령을 사용하여 런타임 오류가 발생할 수 있습니다. 발생할 경우 아래의 업데이트된 호출로 전환해 보십시오.새 셸 명령어 주변에 do catch 문을 사용해야 하지만, 이렇게 하면 예기치 않은 오류를 탐지하는 방법을 찾는 데 시간이 절약될 것입니다.

설명:task.launch()는 던지기 함수가 아니기 때문에 잡을 수 없고, 호출 시 앱이 크래시 되는 경우가 종종 있었습니다.인터넷 검색을 많이 한 결과, 앱이 충돌할 때 제대로 오류를 발생시키는 새로운 함수 task.run()을 위해 프로세스 클래스가 사용되지 않는 task.launch()를 발견했습니다.업데이트된 방법에 대한 자세한 내용은 https://eclecticlight.co/2019/02/02/scripting-in-swift-process-deprecations/를 참조하십시오.

import Foundation

@discardableResult // Add to suppress warnings when you don't want/need a result
func safeShell(_ command: String) throws -> String {
    let task = Process()
    let pipe = Pipe()
    
    task.standardOutput = pipe
    task.standardError = pipe
    task.arguments = ["-c", command]
    task.executableURL = URL(fileURLWithPath: "/bin/zsh") //<--updated
    task.standardInput = nil

    try task.run() //<--updated
    
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output = String(data: data, encoding: .utf8)!
    
    return output
}

예를 들어 다음과 같습니다.

// Example usage capturing error:
do {
    try safeShell("ls -la")
}
catch {
    print("\(error)") //handle or silence the error here
}

// Example usage where you don't care about the error and want a nil back instead
let result = try? safeShell("ls -la")

// Example usage where you don't care about the error or the return value
try? safeShell("ls -la")

참고: 사용 중인 마지막 경우입니다.try?이 결과를 사용하지 않고 있습니다. 어떤 이유에서인지 컴파일러가 경고하고 있습니다. 어떤 이유로 컴파일러는 여전히 경고합니다.@discardableResult. 이런 일은 하다 하다에서만 일어납니다try?이 아니라요tryA 안에 있습니다.do-try-catch블록 또는 던지기 기능 내에서 차단합니다.이겁니다.

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용입니다.

문제는 Bash와 Swift를 혼재시킬 수 없다는 것입니다.명령줄에서 Swift 스크립트를 실행하는 방법을 이미 알고 있으므로 이제 Swift에서 Shell 명령을 실행하는 방법을 추가해야 합니다.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 코드는 Swift를 실행합니다.xcodebuild이겁니다.

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

디렉토리 내용(즉, 디렉토리 내용 검색)의 경우입니다.ls)를 사용하는 것이 좋습니다.NSFileManagerBash 출력 대신 Swift에서 직접 디렉토리를 스캔하면 구문 분석하기가 힘들 수 있습니다.

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

호출 명령에 bash 환경을 사용하려면 고정 버전의 Legoless를 사용하는 다음 bash 함수를 사용하십시오.셸 함수의 결과에서 후행하는 줄바꿈을 제거해야 했습니다.

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

Apple이 .launchPath와 launch()를 모두 추천하지 않았기 때문에 이를 업데이트하기 위해 Swift 4에 대한 업데이트된 유틸리티 기능이 있습니다.

참고: 교체(실행()), 실행 파일에 대한 Apple의 설명서입니다.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")

놀이터에 직접 붙일 수 있어야만 움직이는 것을 볼 수 있습니다.

전체 대본은 레골레스의 답변을 기반으로 합니다.

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

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

여기에 게시된 몇 가지 솔루션을 사용해 본 결과, 명령을 실행하는 가장 좋은 방법은-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")
import Foundation

enum Commands {
  struct Result {
    public let statusCode: Int32
    public let output: String
  }
  
  static func run(_ command: String,
                  environment: [String: String]? = nil,
                  executableURL: String = "/bin/bash",
                  dashc: String = "-c") -> Result {
    // create process
    func create(_ executableURL: String,
                dashc: String,
                environment: [String: String]?) -> Process {
      let process = Process()
      if #available(macOS 10.13, *) {
        process.executableURL = URL(fileURLWithPath: executableURL)
      } else {
        process.launchPath = "/bin/bash"
      }
      if let environment = environment {
        process.environment = environment
      }
      process.arguments = [dashc, command]
      return process
    }
    // run process
    func run(_ process: Process) throws {
      if #available(macOS 10.13, *) {
        try process.run()
      } else {
        process.launch()
      }
      process.waitUntilExit()
    }
    // read data
    func fileHandleData(fileHandle: FileHandle) throws -> String? {
      var outputData: Data?
      if #available(macOS 10.15.4, *) {
        outputData = try fileHandle.readToEnd()
      } else {
        outputData = fileHandle.readDataToEndOfFile()
      }
      if let outputData = outputData {
        return String(data: outputData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
      }
      return nil
    }
    
    let process = create(executableURL, dashc: dashc, environment: environment)
    
    let outputPipe = Pipe()
    process.standardOutput = outputPipe
    
    let errorPipe = Pipe()
    process.standardError = errorPipe
    
    do {
      try run(process)
      
      let outputActual = try fileHandleData(fileHandle: outputPipe.fileHandleForReading) ?? ""
      let errorActual = try fileHandleData(fileHandle: errorPipe.fileHandleForReading) ?? ""
      
      if process.terminationStatus == EXIT_SUCCESS {
        return Result(statusCode: process.terminationStatus, output: outputActual)
      }
      return Result(statusCode: process.terminationStatus, output: errorActual)
    } catch let error {
      return Result(statusCode: process.terminationStatus, output: error.localizedDescription)
    }
  }
}

사용.

let result = Commands.run("ls")
debugPrint(result.output)
debugPrint(result.statusCode)

아니면 속전속결로요.

import Commands

Commands.Bash.system("ls")

스위프트 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
}

다음과 같이 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)
}

프로세스 클래스를 사용하여 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)")
   }
}

스위프트를 만들었어요Exec은 이러한 명령을 실행하기 위한 작은 라이브러리입니다.

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도 있습니다.

다음과 같은 터미널 명령을 실행하는 앱을 많이 보았습니다.

cd /Applications/Theirappname.app/Contents/Resources && do sth here

이 명령은 셸 스크립트를 실행하는 것과 다르지 않으며 앱이 Applications 폴더에 없으면 다음 오류가 발생하므로 올바르게 실행되지 않습니다.No such file or directory: /Applications/Theirappname.app따라서 리소스 폴더에서 실행 파일을 실행하려면 다음 코드를 사용해야 합니다.

func runExec() -> Int32 {
   let task = Process()
   task.arguments = [Bundle.main.url(forResource: "YourExecutablefile", withExtension: "its_extension", subdirectory: "if_exists/")!.path]
   //If it does not have an extension then you just leave it empty
   //You can remove subdirectory if it does not exist
   task.launch()
   task.waitUntilExit()
   return task.terminationStatus
}

실행 파일에 인수가 필요한 경우 코드는 다음과 같습니다.

func runExec() -> Int32 {
        let task = Process()
        task.launchPath = "/bin/bash"
        task.launchPath = Bundle.main.url(forResource: "YourExecutablefile", withExtension: "its_extension", subdirectory: "if_exists")?.path
   //If it does not have an extension then you just leave it empty
   //You can remove subdirectory if it does not exist
        task.arguments = ["arg1","arg2"]
        task.launch()
        task.waitUntilExit()
        return task.terminationStatus
}

NSTask를 Swift로 사용한 기존 Objective-C 코드를 다시 팩터링하는 중이고, 다른 답변에서 누락된 한 가지 핵심은 대량의 stdout/stderr 출력을 어떻게 처리해야 하는가입니다.이렇게 하지 않으면 실행 프로세스에서 중단이 발생할 수 있습니다.

일반적으로 실행하는 명령 중 하나는 stdout과 stderr 모두에 대해 수백KB의 출력을 생성할 수 있습니다.

이 문제를 해결하기 위해 다음과 같이 출력을 버퍼링합니다.

import Foundation

struct ShellScriptExecutor {

    static func runScript(_ script: ShellScript) -> ShellScriptResult {
        var errors: String = ""
        let tempFile = copyToTempFile(script)
        let process = Process()
        let stdout = Pipe()
        let stderr = Pipe()
        var stdoutData = Data.init(capacity: 8192)
        var stderrData = Data.init(capacity: 8192)

        process.standardOutput = stdout
        process.standardError = stderr
        process.executableURL = URL(fileURLWithPath: "/bin/zsh")
        process.arguments = [tempFile]

        do {
            try process.run()

            // Buffer the data while running
            while process.isRunning {
                stdoutData.append(pipeToData(stdout))
                stderrData.append(pipeToData(stderr))
            }

            process.waitUntilExit()

            stdoutData.append(pipeToData(stdout))
            errors = dataToString(stderrData) + pipeToString(stderr)
        }

        catch {
            print("Process failed for " + tempFile + ": " + error.localizedDescription)
        }

        // Clean up
        if !tempFile.isEmpty {
            do {
                try FileManager.default.removeItem(atPath: tempFile)
            }

            catch {
                print("Unable to remove " + tempFile + ": " + error.localizedDescription)
            }
        }

        return ShellScriptResult(stdoutData, script.resultType, errors)
    }

    static private func copyToTempFile(_ script: ShellScript) -> String {
        let tempFile: String = URL(fileURLWithPath: NSTemporaryDirectory())
            .appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString + ".sh", isDirectory: false).path

        if FileManager.default.createFile(atPath: tempFile, contents: Data(script.script.utf8), attributes: nil) {
            return tempFile;
        }
        else {
            return ""
        }
    }

    static private func pipeToString(_ pipe: Pipe) -> String {
        return dataToString(pipeToData(pipe))
    }

    static private func dataToString(_ data: Data) -> String {
        return String(decoding: data, as: UTF8.self)
    }

    static private func pipeToData(_ pipe: Pipe) -> Data {
        return pipe.fileHandleForReading.readDataToEndOfFile()
    }
}

(ShellScript 및 ShellScriptResult는 단순한 래퍼 클래스일 뿐입니다.)

언급URL : https://stackoverflow.com/questions/26971240/how-do-i-run-a-terminal-command-in-a-swift-script-e-g-xcodebuild 입니다.

반응형