Skip to content

Swift library for macOS providing interfaces for both synchronous and asynchronous process execution

License

Notifications You must be signed in to change notification settings

jamf/Subprocess

Repository files navigation

Subprocess

License Build CocoaPods Platform Language Carthage compatible SwiftPM compatible Documentation

Subprocess is a Swift library for macOS providing interfaces for both synchronous and asynchronous process execution. SubprocessMocks can be used in unit tests for quick and highly customizable mocking and verification of Subprocess usage.

Full Documentation

Usage

Subprocess Class

The Subprocess class can be used for command execution.

Command Input

Input for data
let inputData = Data("hello world".utf8)
let data = try await Subprocess.data(for: ["/usr/bin/grep", "hello"], standardInput: inputData)
Input for text
let data = try await Subprocess.data(for: ["/usr/bin/grep", "hello"], standardInput: "hello world")
Input for file URL
let data = try await Subprocess.data(for: ["/usr/bin/grep", "foo"], standardInput: URL(filePath: "/path/to/input/file"))

Command Output

Output as Data
let data = try await Subprocess.data(for: ["/usr/bin/sw_vers"])
Output as String
let string = try await Subprocess.string(for: ["/usr/bin/sw_vers"])
Output as decodable object from JSON
struct LogMessage: Codable {
    var subsystem: String
    var category: String
    var machTimestamp: UInt64
}

let result: [LogMessage] = try await Subprocess.value(for: ["/usr/bin/log", "show", "--style", "json", "--last", "30s"], decoder: JSONDecoder())
Output as decodable object from Property List
struct SystemVersion: Codable {
    enum CodingKeys: String, CodingKey {
        case version = "ProductVersion"
    }
    var version: String
}

let result: SystemVersion = try await Subprocess.value(for: ["/bin/cat", "/System/Library/CoreServices/SystemVersion.plist"], decoder: PropertyListDecoder())
Output mapped to other type
let enabled = try await Subprocess(["/usr/bin/csrutil", "status"]).run().standardOutput.lines.first(where: { $0.contains("enabled") } ) != nil
Output options
let errorText = try await Subprocess.string(for: ["/usr/bin/cat", "/non/existent/file.txt"], options: .returnStandardError)
let outputText = try await Subprocess.string(for: ["/usr/bin/sw_vers"])

async let (standardOutput, standardError, _) = try Subprocess(["/usr/bin/csrutil", "status"]).run()
let combinedOutput = try await [standardOutput.string(), standardError.string()]
Handling output as it is read
let (stream, input) = {
    var input: AsyncStream<UInt8>.Continuation!
    let stream: AsyncStream<UInt8> = AsyncStream { continuation in
        input = continuation
    }

    return (stream, input!)
}()

let subprocess = Subprocess(["/bin/cat"])
let (standardOutput, _, waitForExit) = try subprocess.run(standardInput: stream)

input.yield("hello\n")

Task {
    for await line in standardOutput.lines {
        switch line {
        case "hello":
            input.yield("world\n")
        case "world":
            input.yield("and\nuniverse")
            input.finish()
        case "universe":
            await waitForExit()
            break
        default:
            continue
        }
    }
}
Handling output on termination
let process = Subprocess(["/usr/bin/csrutil", "status"])
let (standardOutput, standardError, waitForExit) = try process.run()
async let (stdout, stderr) = (standardOutput, standardError)
let combinedOutput = await [stdout.data(), stderr.data()]

await waitForExit()

if process.exitCode == 0 {
    // Do something with output data
} else {
    // Handle failure
}
Closure based callbacks
let command: [String] = ...
let process = Subprocess(command)
nonisolated(unsafe) var outputData: Data?
nonisolated(unsafe) var errorData: Data?

// The outputHandler and errorHandler are invoked serially
try process.launch(outputHandler: { data in
    // Handle new data read from stdout
    outputData = data 
}, errorHandler: { data in
    // Handle new data read from stderr
    errorData = data
}, terminationHandler: { process in
    // Handle process termination, all scheduled calls to
    // the outputHandler and errorHandler are guaranteed to
    // have completed.
})
Handing output on termination with a closure
let command: [String] = ...
let process = Subprocess(command)

try process.launch { (process, outputData, errorData) in
    if process.exitCode == 0 {
        // Do something with output data
    } else {
        // Handle failure
    }

Installation

SwiftPM

let package = Package(
    // name, platforms, products, etc.
    dependencies: [
        // other dependencies
        .package(url: "https://github.com/jamf/Subprocess.git", .upToNextMajor(from: "3.0.0")),
    ],
    targets: [
        .target(name: "<target>",
        dependencies: [
            // other dependencies
            .product(name: "Subprocess"),
        ]),
        // other targets
    ]
)

Cocoapods

pod 'Subprocess'

Carthage

github 'jamf/Subprocess'