Skip to content

Commit

Permalink
Merge pull request #20 from jamf/jamf/modern-swift-concurrency-support
Browse files Browse the repository at this point in the history
Add modern Swift Concurrency support
  • Loading branch information
mlink authored Oct 26, 2023
2 parents 69d64d7 + 609630e commit 5ab92a0
Show file tree
Hide file tree
Showing 33 changed files with 1,680 additions and 320 deletions.
14 changes: 10 additions & 4 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,32 @@ on:
jobs:
spm:
name: SwiftPM build and test
runs-on: macos-latest
runs-on: macos-13
steps:
- run: |
sudo xcode-select -s /Applications/Xcode_15.0.app
- uses: actions/checkout@v3
- name: Build swift packages
run: swift build -v
- name: Run tests
run: swift test -v
carthage:
name: Xcode project build and test
runs-on: macos-latest
runs-on: macos-13
steps:
- run: |
sudo xcode-select -s /Applications/Xcode_15.0.app
- uses: actions/checkout@v3
- name: Build xcode project
run: xcodebuild build -scheme 'SubprocessMocks' -derivedDataPath .build
- name: Run tests
run: xcodebuild test -scheme 'Subprocess' -derivedDataPath .build
cocoapods:
name: Pod lib lint
runs-on: macos-latest
runs-on: macos-13
steps:
- run: |
sudo xcode-select -s /Applications/Xcode_15.0.app
- uses: actions/checkout@v3
- name: Lib lint
run: pod lib lint --verbose Subprocess.podspec
run: pod lib lint --verbose Subprocess.podspec --allow-warnings
4 changes: 3 additions & 1 deletion .github/workflows/publish-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ concurrency:
jobs:
build_docs:
name: Build and Archive Docs
runs-on: macos-12
runs-on: macos-13
steps:
- run: |
sudo xcode-select -s /Applications/Xcode_15.0.app
- name: Checkout
uses: actions/checkout@v3

Expand Down
9 changes: 9 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
disabled_rules:
- trailing_whitespace # Xcode automatically adds space for new lines
- line_length # IDE is good at wrapping long lines
- function_body_length
- file_length # doesn't play nice when you need to have private in the same file
- nesting
- large_tuple
- colon # doesn't follow Swift formatting
- type_body_length # XCTest subclasses and arbitrary depending on type
7 changes: 7 additions & 0 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
## Subprocess

Subprocess is a Swift library for macOS providing interfaces for both synchronous and asynchronous process execution.
Subprocess is a Swift library for macOS providing interfaces for external process execution.

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 3.0.0 - 2023-10-13

### Added
- Methods to `Subprocess` that support Swift Concurrency.
- `Subprocess.run(standardInput:options:)` can run interactive commands.

### Changed
- Breaking: `Subprocess.init` no longer accepts an argument for a dispatch queue's quality of service since the underlying implementation now uses Swift Concurrency and not GCD.
- Breaking: `Input`s `text` case no longer accepts an encoding as utf8 is overwhelmingly common. Instead convert the string to data explicitly if an alternate encoding is required.
- `Shell` and `SubprocessError` have been deprecated in favor of using new replacement methods that support Swift Concurrency and that no longer have a synchronized wait.
- Swift 5.9 (Xcode 15) is now the package minimum required to build.

## 2.0.0 - 2021-07-01

### Changed
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2020 Jamf Open Source Community
Copyright (c) 2023 Jamf

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
42 changes: 20 additions & 22 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
{
"object": {
"pins": [
{
"package": "SwiftDocCPlugin",
"repositoryURL": "https://github.com/apple/swift-docc-plugin",
"state": {
"branch": null,
"revision": "9b1258905c21fc1b97bf03d1b4ca12c4ec4e5fda",
"version": "1.2.0"
}
},
{
"package": "SymbolKit",
"repositoryURL": "https://github.com/apple/swift-docc-symbolkit",
"state": {
"branch": null,
"revision": "b45d1f2ed151d057b54504d653e0da5552844e34",
"version": "1.0.0"
}
"pins" : [
{
"identity" : "swift-docc-plugin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-docc-plugin",
"state" : {
"revision" : "26ac5758409154cc448d7ab82389c520fa8a8247",
"version" : "1.3.0"
}
]
},
"version": 1
},
{
"identity" : "swift-docc-symbolkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-docc-symbolkit",
"state" : {
"revision" : "b45d1f2ed151d057b54504d653e0da5552844e34",
"version" : "1.0.0"
}
}
],
"version" : 2
}
6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// swift-tools-version:5.1
// swift-tools-version: 5.9

import PackageDescription

let package = Package(
name: "Subprocess",
platforms: [ .macOS(.v10_13) ],
platforms: [ .macOS("10.15.4") ],
products: [
.library(
name: "Subprocess",
Expand All @@ -28,7 +28,7 @@ let package = Package(
)
],
dependencies: [
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
.package(url: "https://github.com/apple/swift-docc-plugin", .upToNextMajor(from: "1.0.0"))
],
targets: [
.target(
Expand Down
130 changes: 85 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ Subprocess is a Swift library for macOS providing interfaces for both synchronou
SubprocessMocks can be used in unit tests for quick and highly customizable mocking and verification of Subprocess usage.

- [Usage](#usage)
- [Shell](#shell-class)
- [Input](#command-input) - [Data](#input-for-data), [Text](#input-for-text), [File](#input-for-file-url)
- [Output](#command-output) - [Data](#output-as-data), [Text](#output-as-string), [JSON](#output-as-json), [Decodable JSON object](#output-as-decodable-object-from-json), [Property list](#output-as-property-list), [Decodable property list object](#output-as-decodable-object-from-property-list)
- [Subprocess](#subprocess-class)
- [Subprocess Class](#subprocess-class)
- [Command Input](#command-input) - [Data](#input-for-data), [Text](#input-for-text), [File](#input-for-file-url)
- [Command Output](#command-output) - [Data](#output-as-data), [Text](#output-as-string), [Decodable JSON object](#output-as-decodable-object-from-json), [Decodable property list object](#output-as-decodable-object-from-property-list)
- [Installation](#installation)
- [SwiftPM](#swiftpm)
- [Cocoapods](#cocoapods)
Expand All @@ -24,44 +23,34 @@ SubprocessMocks can be used in unit tests for quick and highly customizable mock
[Full Documentation](./docs/index.html)

# Usage
### Shell Class
The Shell class can be used for synchronous command execution.
### Subprocess Class
The `Subprocess` class can be used for command execution.

#### Command Input

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

#### Command Output

###### Output as Data
```swift
let data = try Shell(["/usr/bin/sw_vers"]).exec()
let data = try await Subprocess.data(for: ["/usr/bin/sw_vers"])
```
###### Output as String
```swift
let text = try Shell(["/usr/bin/sw_vers"]).exec(encoding: .utf8)
```
###### Output as JSON (Array or Dictionary)
```swift
let command = ["/usr/bin/log", "show", "--style", "json", "--last", "5m"]
let logs: [[String: Any]] = try Shell(command).execJSON())
let string = try await Subprocess.string(for: ["/usr/bin/sw_vers"])
```
###### Output as decodable object from JSON
```swift
Expand All @@ -70,13 +59,8 @@ struct LogMessage: Codable {
var category: String
var machTimestamp: UInt64
}
let command = ["/usr/bin/log", "show", "--style", "json", "--last", "5m"]
let logs: [LogMessage] = try Shell(command).exec(decoder: JSONDecoder())
```
###### Output as Property List (Array or Dictionary)
```swift
let command = ["/bin/cat", "/System/Library/CoreServices/SystemVersion.plist"]
let dictionary: [String: Any] = try Shell(command).execPropertyList())

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
```swift
Expand All @@ -86,26 +70,71 @@ struct SystemVersion: Codable {
}
var version: String
}
let command = ["/bin/cat", "/System/Library/CoreServices/SystemVersion.plist"]
let result: SystemVersion = try Shell(command).exec(decoder: PropertyListDecoder())

let result: SystemVersion = try await Subprocess.value(for: ["/bin/cat", "/System/Library/CoreServices/SystemVersion.plist"], decoder: PropertyListDecoder())
```
###### Output mapped to other type
```swift
let enabled = try Shell(["csrutil", "status"]).exec(encoding: .utf8) { _, txt in txt.contains("enabled") }
let enabled = try await Subprocess(["/usr/bin/csrutil", "status"]).run().standardOutput.lines.first(where: { $0.contains("enabled") } ) != nil
```

###### Output options
```swift
let command: [String] = ...
let errorText = try Shell(command).exec(options: .stderr, encoding: .utf8)
let outputText = try Shell(command).exec(options: .stdout, encoding: .utf8)
let combinedData = try Shell(command).exec(options: .combined)
```
### Subprocess Class
The Subprocess class can be used for asynchronous command execution.
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
```swift
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
```swift
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
```swift
let command: [String] = ...
let process = Subprocess(command)

Expand All @@ -120,7 +149,7 @@ try process.launch(outputHandler: { data in
// have completed.
})
```
###### Handling output on termination
###### Handing output on termination with a closure
```swift
let command: [String] = ...
let process = Subprocess(command)
Expand All @@ -131,15 +160,26 @@ try process.launch { (process, outputData, errorData) in
} else {
// Handle failure
}
}
```

## Installation
### SwiftPM
```swift
dependencies: [
.package(url: "https://github.com/jamf/Subprocess.git", from: "1.0.0")
]
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
```ruby
Expand Down
Loading

0 comments on commit 5ab92a0

Please sign in to comment.