Skip to content

Instantly share code, notes, and snippets.

@norio-nomura
Created November 11, 2017 07:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save norio-nomura/90a374d33c5178fdbc336b1e50cf74dc to your computer and use it in GitHub Desktop.
Save norio-nomura/90a374d33c5178fdbc336b1e50cf74dc to your computer and use it in GitHub Desktop.
import Foundation
enum Error: CustomStringConvertible, Swift.Error {
case failed(subcommand: String, message: String, status: Int32)
var description: String {
switch self {
case let .failed(subcommand: subcommand, message: message, status: status):
return "`\(subcommand)` failed with status: \(status)\n\(message)"
}
}
}
private final class Pipe {
let fileHandleForReading: FileHandle
let fileHandleForWriting: FileHandle
let readingSemaphore = DispatchSemaphore(value: 1)
init() throws {
var fileDescriptors: [Int32] = [0, 0]
guard 0 == pipe(&fileDescriptors) else {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
}
fileHandleForReading = .init(fileDescriptor: fileDescriptors[0], closeOnDealloc: true)
fileHandleForWriting = .init(fileDescriptor: fileDescriptors[1], closeOnDealloc: true)
}
convenience init(readabilityHandler: @escaping (Data) -> Void) throws {
try self.init()
var error: Int32 = 0
let channel = DispatchIO(type: .stream,
fileDescriptor: fileHandleForReading.fileDescriptor,
queue: .global()) { error = $0 }
guard error == 0 else {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(error))
}
channel.setLimit(lowWater: 1)
channel.read(offset: 0, length: .max, queue: .global()) { done, data, error in
if let data = data {
data.withUnsafeBytes {
readabilityHandler(.init(bytes: $0, count: data.count))
}
}
if error != 0 {
fatalError("\(NSError(domain: NSPOSIXErrorDomain, code: Int(error)))")
}
if done {
channel.close()
self.readingSemaphore.signal()
}
}
}
func waitUntilEndOfFile() {
readingSemaphore.wait()
}
}
@discardableResult
func execute(_ arguments: [String],
at url: URL? = nil,
environment: [String: String]? = nil,
input: Data? = nil,
verbose: Bool = false) throws -> String {
if verbose {
print("- " + arguments.joined(separator: " "))
}
let process = Process()
process.launchPath = "/usr/bin/env"
process.arguments = arguments
if let url = url {
process.currentDirectoryURL = url
}
if let environment = environment {
process.environment = ProcessInfo.processInfo.environment.merging(environment) {
(_, new) in new
}
}
var stdoutData = Data()
let stdoutPipe = try Pipe() { data in
if verbose {
FileHandle.standardOutput.write(data)
}
stdoutData.append(data)
}
var stderrData = Data()
let stderrPipe = try Pipe() { data in
if verbose {
FileHandle.standardError.write(data)
}
stderrData.append(data)
}
process.standardOutput = stdoutPipe.fileHandleForWriting
process.standardError = stderrPipe.fileHandleForWriting
if let input = input {
let stdinPipe = try Pipe()
process.standardInput = stdinPipe.fileHandleForReading
process.launch()
stdinPipe.fileHandleForWriting.write(input)
stdinPipe.fileHandleForWriting.closeFile()
} else {
process.launch()
}
process.waitUntilExit()
stdoutPipe.waitUntilEndOfFile()
stderrPipe.waitUntilEndOfFile()
if process.terminationStatus != 0 {
throw Error.failed(subcommand: arguments.joined(separator: " "),
message: String(data: stderrData, encoding: .utf8) ?? "",
status: process.terminationStatus)
}
return String(data: stdoutData, encoding: .utf8) ?? ""
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment