-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5fa39b0
commit e201b18
Showing
5 changed files
with
547 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
.DS_Store | ||
/.build | ||
/Packages | ||
/*.xcodeproj | ||
xcuserdata/ | ||
DerivedData/ | ||
.swiftpm/config/registries.json | ||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata | ||
.netrc | ||
# Xcode | ||
# | ||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore | ||
|
||
## User settings | ||
xcuserdata/ | ||
|
||
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) | ||
*.xcscmblueprint | ||
*.xccheckout | ||
|
||
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) | ||
build/ | ||
DerivedData/ | ||
*.moved-aside | ||
*.pbxuser | ||
!default.pbxuser | ||
*.mode1v3 | ||
!default.mode1v3 | ||
*.mode2v3 | ||
!default.mode2v3 | ||
*.perspectivev3 | ||
!default.perspectivev3 | ||
|
||
## Obj-C/Swift specific | ||
*.hmap | ||
|
||
## App packaging | ||
*.ipa | ||
*.dSYM.zip | ||
*.dSYM | ||
|
||
## Playgrounds | ||
timeline.xctimeline | ||
playground.xcworkspace | ||
|
||
# Swift Package Manager | ||
# | ||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. | ||
# Packages/ | ||
# Package.pins | ||
# Package.resolved | ||
# *.xcodeproj | ||
# | ||
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata | ||
# hence it is not needed unless you have added a package configuration file to your project | ||
# .swiftpm | ||
|
||
.build/ | ||
|
||
# CocoaPods | ||
# | ||
# We recommend against adding the Pods directory to your .gitignore. However | ||
# you should judge for yourself, the pros and cons are mentioned at: | ||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control | ||
# | ||
# Pods/ | ||
# | ||
# Add this line if you want to avoid checking in source code from the Xcode workspace | ||
# *.xcworkspace | ||
|
||
# Carthage | ||
# | ||
# Add this line if you want to avoid checking in source code from Carthage dependencies. | ||
# Carthage/Checkouts | ||
|
||
Carthage/Build/ | ||
|
||
# Accio dependency management | ||
Dependencies/ | ||
.accio/ | ||
|
||
# fastlane | ||
# | ||
# It is recommended to not store the screenshots in the git repo. | ||
# Instead, use fastlane to re-generate the screenshots whenever they are needed. | ||
# For more information about the recommended setup visit: | ||
# https://docs.fastlane.tools/best-practices/source-control/#source-control | ||
|
||
fastlane/report.xml | ||
fastlane/Preview.html | ||
fastlane/screenshots/**/*.png | ||
fastlane/test_output | ||
|
||
# Code Injection | ||
# | ||
# After new code Injection tools there's a generated folder /iOSInjectionProject | ||
# https://github.com/johnno1962/injectionforxcode | ||
|
||
iOSInjectionProject/ | ||
|
||
# VS Code | ||
.vscode |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// swift-tools-version: 5.7 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "Coquille", | ||
platforms: [ | ||
.macOS(.v10_15) | ||
], | ||
products: [ | ||
.library( | ||
name: "Coquille", | ||
targets: ["Coquille"]) | ||
], | ||
dependencies: [], | ||
targets: [ | ||
.target( | ||
name: "Coquille", | ||
dependencies: []), | ||
.testTarget( | ||
name: "CoquilleTests", | ||
dependencies: ["Coquille"]), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
# Coquille | ||
|
||
A simple Swift wrapper around `Process` supporting Swift Concurrency and streamed output from `stdout` and `stderr`. | ||
|
||
## Requirements | ||
|
||
macOS 10.15+ | ||
|
||
## Installation | ||
|
||
Add Coquille to your project using Xcode (File > Add Packages...) or by adding it to your project's `Package.swift` file: | ||
|
||
dependencies: [ | ||
.package(url: "https://github.com/alexrozanski/Coquille.git", from: "0.1.0") | ||
] | ||
|
||
## Usage | ||
|
||
Coquille exposes its own `Process` class which you can interact with to execute commands. `Process.run()` is an `async` function so you can just `await` the exit code: | ||
|
||
```swift | ||
import Coquille | ||
|
||
let process = Process(commandString: "pwd")) | ||
_ = try await process.run() // Prints `pwd` to `stdout` | ||
|
||
// Use `command:` for more easily working with variable command-line arguments | ||
let deps = ["numpy", "torch"] | ||
let process = Process(command: .init("python3", arguments: ["-m", "pip", "install"] + deps))) | ||
_ = try await process.run() | ||
``` | ||
|
||
### I/O | ||
|
||
By default `Process` pipes output from the spawned process to `stdout` and `stderr`. This can be configured with `printStdout` and `printStderr`: | ||
|
||
```swift | ||
import Coquille | ||
|
||
let process = Process(commandString: "brew install wget", printStderr: false)) | ||
_ = try await process.run() // Pipes standard output to `stdout` but will not pipe error output to `stderr` | ||
``` | ||
|
||
You can also pass an `OutputHandler` for both stdout and stderr which will stream contents from both: | ||
|
||
```swift | ||
import Coquille | ||
|
||
let process = Process( | ||
commandString: "swift build", | ||
stdout: { stdout in | ||
... | ||
}, | ||
stderr: { stderr in | ||
... | ||
}) | ||
_ = try await process.run() // Streams standard and error output to the handlers provided to `stdout:` and `stderr:` | ||
``` | ||
|
||
### Exit Codes | ||
|
||
```swift | ||
// `isSuccess` can be used to test the exit code for success | ||
let hasRuby = (try await Process(commandString: "which ruby").run()).isSuccess | ||
|
||
// Use `errorCode` to get a nonzero exit code | ||
if let errorCode = (try await Process(commandString: "swift build").run()).errorCode { | ||
switch errorCode { | ||
case 127: | ||
// Command not found | ||
default: | ||
... | ||
} | ||
} | ||
``` | ||
|
||
## Acknowledgements | ||
|
||
Thanks to [Ben Chatelain](https://github.com/phatblat) for their [blog post](https://phatbl.at/2019/01/08/intercepting-stdout-in-swift.html) on intercepting stdout, used | ||
to implement some of the tests in the test suite. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
import Foundation | ||
|
||
public class Process { | ||
public typealias OutputHandler = (String) -> Void | ||
|
||
public enum Status { | ||
case success | ||
case failure(_ code: Int32) | ||
|
||
var isSuccess: Bool { | ||
switch self { | ||
case .success: return true | ||
case .failure: return false | ||
} | ||
} | ||
|
||
var isFailure: Bool { | ||
switch self { | ||
case .success: return false | ||
case .failure: return true | ||
} | ||
} | ||
|
||
var errorCode: Int32? { | ||
switch self { | ||
case .success: return nil | ||
case .failure(let errorCode): return errorCode | ||
} | ||
} | ||
} | ||
|
||
public struct Command { | ||
let name: String | ||
let arguments: [String] | ||
|
||
init(_ name: String, arguments: [String]) { | ||
self.name = name | ||
self.arguments = arguments | ||
} | ||
} | ||
|
||
public enum Output { | ||
case stdout | ||
case stderr | ||
case handler(OutputHandler) | ||
} | ||
|
||
let arguments: [String] | ||
|
||
let stdout: Output? | ||
let stderr: Output? | ||
|
||
// MARK: - Initializers | ||
|
||
public init(command: Command) { | ||
self.arguments = [command.name] + command.arguments | ||
self.stdout = .stdout | ||
self.stderr = .stderr | ||
} | ||
|
||
public init(command: Command, printStdout: Bool = true, printStderr: Bool = true) { | ||
self.arguments = [command.name] + command.arguments | ||
self.stdout = printStdout ? .stdout : nil | ||
self.stderr = printStderr ? .stderr : nil | ||
} | ||
|
||
public init(command: Command, stdout: OutputHandler? = nil) { | ||
self.arguments = [command.name] + command.arguments | ||
self.stdout = stdout.map { .handler($0) } | ||
self.stderr = nil | ||
} | ||
|
||
public init(command: Command, stdout: OutputHandler? = nil, stderr: OutputHandler? = nil) { | ||
self.arguments = [command.name] + command.arguments | ||
self.stdout = stdout.map { .handler($0) } | ||
self.stderr = stderr.map { .handler($0) } | ||
} | ||
|
||
// MARK: - Convenience Initializers | ||
|
||
public init(commandString: String) { | ||
self.arguments = commandString.components(separatedBy: " ") | ||
self.stdout = .stdout | ||
self.stderr = .stderr | ||
} | ||
|
||
public init(commandString: String, printStdout: Bool = true, printStderr: Bool = true) { | ||
self.arguments = commandString.components(separatedBy: " ") | ||
self.stdout = printStdout ? .stdout : nil | ||
self.stderr = printStderr ? .stderr : nil | ||
} | ||
|
||
|
||
public init(commandString: String, stdout: OutputHandler? = nil) { | ||
self.arguments = commandString.components(separatedBy: " ") | ||
self.stdout = stdout.map { .handler($0) } | ||
self.stderr = nil | ||
} | ||
|
||
public init(commandString: String, stdout: OutputHandler? = nil, stderr: OutputHandler? = nil) { | ||
self.arguments = commandString.components(separatedBy: " ") | ||
self.stdout = stdout.map { .handler($0) } | ||
self.stderr = stderr.map { .handler($0) } | ||
} | ||
|
||
// MARK: - Running tasks | ||
|
||
public func run() async throws -> Status { | ||
return try await withCheckedThrowingContinuation { continuation in | ||
do { | ||
try _run { status in | ||
continuation.resume(returning: status) | ||
} | ||
} catch { | ||
continuation.resume(throwing: error) | ||
} | ||
} | ||
} | ||
|
||
private func _run(with completionHandler: (Status) -> Void) throws { | ||
let _process = Foundation.Process() | ||
_process.launchPath = "/usr/bin/env" | ||
_process.arguments = arguments | ||
|
||
let stdoutPipe = Pipe() | ||
_process.standardOutput = stdoutPipe | ||
let stderrPipe = Pipe() | ||
_process.standardError = stderrPipe | ||
|
||
stdoutPipe.output(to: stdout) | ||
stderrPipe.output(to: stderr) | ||
|
||
try _process.run() | ||
_process.waitUntilExit() | ||
|
||
let exitStatus = _process.terminationStatus | ||
if exitStatus == 0 { | ||
completionHandler(.success) | ||
} else { | ||
completionHandler(.failure(exitStatus)) | ||
} | ||
} | ||
} | ||
|
||
extension Pipe { | ||
fileprivate func output(to output: Coquille.Process.Output?) { | ||
guard let output else { return } | ||
|
||
fileHandleForReading.readabilityHandler = { pipe in | ||
switch output { | ||
case .stdout: | ||
if #available(macOS 11, *) { | ||
try? FileHandle.standardOutput.write(contentsOf: pipe.availableData) | ||
} else { | ||
FileHandle.standardOutput.write(pipe.availableData) | ||
} | ||
case .stderr: | ||
if #available(macOS 11, *) { | ||
try? FileHandle.standardError.write(contentsOf: pipe.availableData) | ||
} else { | ||
FileHandle.standardError.write(pipe.availableData) | ||
} | ||
case .handler(let handler): | ||
// This closure is called frequently with empty data, so only pass this on if we have something. | ||
if let string = String(data: pipe.availableData, encoding: .utf8), !string.isEmpty { | ||
handler(string) | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.