Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add modern Swift Concurrency support #20

Merged
merged 8 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
mlink marked this conversation as resolved.
Show resolved Hide resolved
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.

13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
## 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 no longer for a synchronized wait.

mlink marked this conversation as resolved.
Show resolved Hide resolved
## 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