-
Notifications
You must be signed in to change notification settings - Fork 3
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
Full Queue Developer
committed
Apr 19, 2022
0 parents
commit 4b9e5a6
Showing
19 changed files
with
763 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,41 @@ | ||
name: CI | ||
|
||
on: | ||
push: | ||
branches: "**" | ||
pull_request: | ||
|
||
jobs: | ||
build: | ||
name: Swift ${{ matrix.swift }} on ${{ matrix.os }} | ||
runs-on: ${{ matrix.os }} | ||
strategy: | ||
fail-fast: false | ||
matrix: | ||
include: | ||
- os: macos-11.0 | ||
swift: "5.5" | ||
- os: macos-11.0 | ||
swift: "5.6" | ||
# difficult to get a macos 12 runner on GitHub | ||
# - os: macos-12.0 | ||
# swift: "5.5" | ||
# - os: macos-12.0 | ||
# swift: "5.6" | ||
- os: ubuntu-20.04 | ||
swift: "5.5" | ||
- os: ubuntu-20.04 | ||
swift: "5.6" | ||
|
||
steps: | ||
- uses: fwal/[email protected] | ||
with: | ||
swift-version: ${{ matrix.swift }} | ||
- run: swift --version | ||
- uses: actions/checkout@v2 | ||
|
||
- name: test | ||
run: swift test | ||
|
||
- name: build for release | ||
run: swift build -c release |
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,3 @@ | ||
.DS_Store | ||
.build | ||
.swiftpm |
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,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2022 Full Queue Developer | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
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,16 @@ | ||
{ | ||
"object": { | ||
"pins": [ | ||
{ | ||
"package": "Rainbow", | ||
"repositoryURL": "https://github.com/onevcat/Rainbow", | ||
"state": { | ||
"branch": null, | ||
"revision": "e0dada9cd44e3fa7ec3b867e49a8ddbf543e3df3", | ||
"version": "4.0.1" | ||
} | ||
} | ||
] | ||
}, | ||
"version": 1 | ||
} |
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,19 @@ | ||
// swift-tools-version:5.5 | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "Sh", | ||
platforms: [ | ||
.macOS(.v11), | ||
], | ||
products: [ | ||
.library(name: "Sh", targets: ["Sh"]), | ||
], | ||
dependencies: [ | ||
.package(url: "https://github.com/onevcat/Rainbow", from: "4.0.0"), | ||
], | ||
targets: [ | ||
.target(name: "Sh", dependencies: ["Rainbow"]), | ||
.testTarget(name: "ShTests", dependencies: ["Sh"]), | ||
]) |
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,91 @@ | ||
# Sh | ||
|
||
Who wants to use Bash or Ruby scripts to maintain your Swift project? Not me. Let's use Swift. | ||
|
||
Sh lets you reason about your script in Swift, easily calling shell commands and using their output in your Swift program. Or when orchestrating a build script, simply redirect all output to the terminal, a log file, or `/dev/null`. | ||
|
||
## Motivation | ||
|
||
Bash scripts have gotten us pretty far, but it's difficult reasoning about control flow. And there's no type safety. Many command line tools already have decent interfaces, it's just the control flow of that could use some improvements. Sh solves this by relying on Swift control flow. | ||
|
||
## Installation | ||
|
||
Add Sh as a dependency in your `Package.swift` | ||
|
||
``` | ||
dependencies: [ | ||
.package(url: "https://github.com/FullQueueDeveloper/Sh.git", from: "1.0.0"), | ||
] | ||
``` | ||
|
||
## Writing scripts: | ||
|
||
### Fetching data from the shell | ||
|
||
Here is a simple example, where we ask the shell for the date, formatted as seconds since 1970. We then parse a `Foundation.TimeInterval`, since it conforms to `Codable`. Last, we construct a `Data`, and print it. | ||
|
||
import Sh | ||
import Foundation | ||
|
||
let timeInterval = try sh(TimeInterval.self, "date +%s") | ||
let date = Date(timeIntervalSince1970: timeInterval) | ||
print("The date is \(date).") | ||
|
||
A more substantial example might query `op` or `lpass` for a secret, or query `terraform output` for information about our infrastructure, or query Apple's `agvtool` for Apple version info of our Xcode project. | ||
|
||
### Long running scripts | ||
|
||
This file might live in `scripts/Sources/pre-commit/main.swift`. Perhaps we want to run our tests, and confirm that the release build succeeds as well. Perhaps we want to see the output of `swift test` in our terminal so we can react to it, but we don't really care to immediately see any release build output, happy to send it to a log file. | ||
|
||
import Sh | ||
import Foundation | ||
|
||
try sh(.terminal, "swift test") | ||
try sh(.file("logs/build.log"), "swift build -c release") | ||
|
||
## Architecture | ||
|
||
Sh adds convenience extensions to `Foundation.Process`. | ||
|
||
### Construction | ||
|
||
Sh makes it easier to construct a `Foundation.Process`. | ||
|
||
init(cmd: String, environment: [String: String] = [:], workingDirectory: String? = nil) | ||
|
||
### Run | ||
|
||
Sh makes it easier to run a `Process`. The basic method runs the process, and returns whatever is in standard output as a `Data?` | ||
|
||
func runReturningData() throws -> Data? | ||
|
||
Sh adds some helper methods that build on this. `runReturningTrimmedString` parses the `Data` as a `String` and trims the whitespace. | ||
|
||
try Process("echo hello").runReturningTrimmedString() // returns "hello" | ||
|
||
Sh can also parse JSON output. Given a simple `struct`: | ||
|
||
struct Simple: Decodable { | ||
let greeting: String | ||
} | ||
|
||
We can parse the output like this: | ||
|
||
let simple = try sh(Simple.self, #"echo '{"greeting": "hello"}'"#) | ||
print(simple.greeting) // prints "hello" | ||
|
||
## Async/await | ||
|
||
Yes, Sh supports Swift's `async`/`await`. All methods have a corresponding `async` version. | ||
|
||
## Goals: | ||
|
||
- Enable calling command line tools easily from Swift, since Swift offers a nicer type system than Bash or Zsh. | ||
- Allow easy variable substitution in shell calls, and what was run in the shell can be announced to the terminal, for easy copy-paste | ||
|
||
## Related Projects | ||
|
||
This package by itself does not try to provide a domain specific language for various tools. But there is a growing list of Sh-powered wrappers that offer a nicer API for some command line tools. | ||
|
||
- [ShGit](https://github.com/FullQueueDeveloper/ShGit) for a Sh wrapper around Git. | ||
- [Sh1Password](https://github.com/FullQueueDeveloper/Sh1Password) for a Sh wrapper around 1Password's CLI version 2. |
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,8 @@ | ||
import Foundation | ||
|
||
extension Data { | ||
|
||
public func asJSON<D: Decodable>(decoding type: D.Type, using jsonDecoder: JSONDecoder = .init()) throws -> D { | ||
try jsonDecoder.decode(type, from: self) | ||
} | ||
} |
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,8 @@ | ||
import Foundation | ||
|
||
extension Data { | ||
|
||
public func asTrimmedString(encoding: String.Encoding = .utf8) -> String? { | ||
String(data: self, encoding: encoding)?.trimmingCharacters(in: .whitespacesAndNewlines) | ||
} | ||
} |
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,12 @@ | ||
import Foundation | ||
|
||
public enum Errors: Error, LocalizedError { | ||
case unexpectedNilDataError | ||
|
||
public var errorDescription: String? { | ||
switch self { | ||
case .unexpectedNilDataError: | ||
return "Expected data, but there wasn't any" | ||
} | ||
} | ||
} |
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,29 @@ | ||
import Foundation | ||
|
||
extension Process { | ||
|
||
public convenience init(cmd: String, environment: [String: String] = [:], workingDirectory: String? = nil) { | ||
self.init() | ||
self.executableURL = URL(fileURLWithPath: "/bin/sh") | ||
self.arguments = ["-c", cmd] | ||
self.environment = ProcessInfo.processInfo.environment.combine(with: environment) | ||
if let workingDirectory = workingDirectory { | ||
self.currentDirectoryURL = URL(fileURLWithPath: workingDirectory) | ||
} | ||
} | ||
} | ||
|
||
private extension Dictionary where Key == String, Value == String { | ||
|
||
func combine(with overrides: [String: String]?) -> [String: String] { | ||
guard let overrides = overrides else { | ||
return self | ||
} | ||
|
||
var result = self | ||
for pair in overrides { | ||
result[pair.key] = pair.value | ||
} | ||
return result | ||
} | ||
} |
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,79 @@ | ||
import Foundation | ||
|
||
extension Process { | ||
|
||
public func runRedirectingAllOutput(to sink: Sink) throws { | ||
try self.redirectAllOutput(to: sink) | ||
try self.run() | ||
self.waitUntilExit() | ||
if let terminationError = terminationError { | ||
throw terminationError | ||
} | ||
} | ||
|
||
public func runRedirectingAllOutput(to sink: Sink) async throws { | ||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in | ||
do { | ||
try self.redirectAllOutput(to: sink) | ||
} catch { | ||
continuation.resume(throwing: error) | ||
return | ||
} | ||
|
||
self.terminationHandler = { process in | ||
if let terminationError = process.terminationError { | ||
continuation.resume(throwing: terminationError) | ||
} else { | ||
continuation.resume() | ||
} | ||
} | ||
|
||
do { | ||
try self.run() | ||
} catch { | ||
continuation.resume(throwing: error) | ||
} | ||
} | ||
} | ||
|
||
private func redirectAllOutput(to sink: Sink) throws { | ||
switch sink { | ||
case .terminal: | ||
self.redirectAllOutputToTerminal() | ||
case .file(path: let path): | ||
try self.redirectAllOutputToFile(path: path) | ||
case .null: | ||
self.redirectAllOutputToNullDevice() | ||
} | ||
} | ||
|
||
private func redirectAllOutputToTerminal() { | ||
self.standardOutput = FileHandle.standardOutput | ||
self.standardError = FileHandle.standardError | ||
} | ||
|
||
private func redirectAllOutputToNullDevice() { | ||
self.standardOutput = FileHandle.nullDevice | ||
self.standardError = FileHandle.nullDevice | ||
} | ||
|
||
private func redirectAllOutputToFile(path: String) throws { | ||
|
||
guard FileManager.default.createFile(atPath: path, contents: Data()) else { | ||
struct CouldNotCreateFile: Error { | ||
let path: String | ||
} | ||
throw CouldNotCreateFile(path: path) | ||
} | ||
|
||
guard let fileHandle = FileHandle(forWritingAtPath: path) else { | ||
struct CouldNotOpenFileForWriting: Error { | ||
let path: String | ||
} | ||
throw CouldNotOpenFileForWriting(path: path) | ||
} | ||
|
||
self.standardError = fileHandle | ||
self.standardOutput = fileHandle | ||
} | ||
} |
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,46 @@ | ||
import Foundation | ||
|
||
extension Process { | ||
|
||
public typealias AllOutput = (stdOut: Data?, stdErr: Data?, terminationError: TerminationError?) | ||
|
||
public func runReturningAllOutput() throws -> AllOutput { | ||
let stdOut = Pipe() | ||
let stdErr = Pipe() | ||
self.standardOutput = stdOut | ||
self.standardError = stdErr | ||
|
||
try self.run() | ||
self.waitUntilExit() | ||
|
||
let stdOutData = try stdOut.fileHandleForReading.readToEnd() | ||
let stdErrData = try stdErr.fileHandleForReading.readToEnd() | ||
return (stdOut: stdOutData, stdErr: stdErrData, terminationError: terminationError) | ||
} | ||
|
||
public func runReturningAllOutput() async throws -> AllOutput { | ||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<AllOutput, Error>) in | ||
let stdOut = Pipe() | ||
let stdErr = Pipe() | ||
self.standardOutput = stdOut | ||
self.standardError = stdErr | ||
|
||
self.terminationHandler = { process in | ||
|
||
do { | ||
let stdOutData = try stdOut.fileHandleForReading.readToEnd() | ||
let stdErrData = try stdErr.fileHandleForReading.readToEnd() | ||
continuation.resume(with: .success((stdOutData, stdErrData, process.terminationError))) | ||
} catch { | ||
continuation.resume(with: .failure(error)) | ||
} | ||
} | ||
|
||
do { | ||
try self.run() | ||
} catch { | ||
continuation.resume(with: .failure(error)) | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.