Skip to content

Commit f4c31aa

Browse files
committed
Initial
0 parents  commit f4c31aa

File tree

9 files changed

+470
-0
lines changed

9 files changed

+470
-0
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.DS_Store
2+
.build
3+
.swiftpm
4+
Packages
5+
*.xcodeproj

Dockerfile

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
FROM swift
2+
WORKDIR /app
3+
COPY . ./
4+
CMD swift package clean
5+
CMD swift test --parallel

LICENSE

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2+
Version 2, December 2004
3+
4+
Copyright (C) 2018-2019 Binary Birds
5+
6+
Authors:
7+
8+
Tibor Bodecs <[email protected]>
9+
10+
Everyone is permitted to copy and distribute verbatim or modified
11+
copies of this license document, and changing it is allowed as long
12+
as the name is changed.
13+
14+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
15+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
16+
17+
0. You just DO WHAT THE FUCK YOU WANT TO.

Package.swift

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// swift-tools-version:5.2
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "shell-kit",
6+
products: [
7+
.library(name: "ShellKit", targets: ["ShellKit"]),
8+
.library(name: "ShellKitDynamic", type: .dynamic, targets: ["ShellKit"])
9+
],
10+
targets: [
11+
.target(name: "ShellKit", dependencies: []),
12+
.testTarget(name: "ShellKitTests", dependencies: ["ShellKit"]),
13+
]
14+
)

README.md

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# ShellKit (🥚)
2+
3+
Shell is a simple package that gives you the ability to call shell commands through Swift.
4+
5+
## Usage
6+
7+
Run (sync):
8+
9+
```swift
10+
import ShellKit
11+
12+
let output = try Shell().run("ls ~")
13+
```
14+
15+
Run (async):
16+
17+
```swift
18+
import ShellKit
19+
20+
Shell().run("sleep 2 && ls ~") { result, error in
21+
//...
22+
}
23+
```
24+
25+
Shell (bash) with environment variables:
26+
27+
```swift
28+
import ShellKit
29+
30+
let shell = Shell("/bin/bash", env: ["ENV_SAMPLE_KEY": "Hello world!"])
31+
let out = try shell.run("echo $ENV_SAMPLE_KEY")
32+
```
33+
34+
You can even set custom ouptut & error handlers.
35+
36+
37+
38+
## Install
39+
40+
Just use the [Swift Package Manager](https://theswiftdev.com/2017/11/09/swift-package-manager-tutorial/) as usual:
41+
42+
```swift
43+
.package(url: "https://github.com/binarybirds/shell-kit", from: "1.0.0"),
44+
```
45+
46+
Don't forget to add "ShellKit" to your target as a dependency:
47+
48+
```swift
49+
.product(name: "ShellKit", package: "shell-kit"),
50+
```
51+
52+
That's it.
53+
54+
55+
## License
56+
57+
[WTFPL](LICENSE) - Do what the fuck you want to.

Sources/ShellKit/Shell.swift

+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/**
2+
Shell.swift
3+
ShellKit
4+
5+
Created by Tibor Bödecs on 2018.12.31.
6+
Copyright Binary Birds. All rights reserved.
7+
*/
8+
9+
import Foundation
10+
import Dispatch
11+
12+
#if os(macOS)
13+
private extension FileHandle {
14+
15+
// checks if the FileHandle is a standard one (STDOUT, STDIN, STDERR)
16+
var isStandard: Bool {
17+
return self === FileHandle.standardOutput ||
18+
self === FileHandle.standardError ||
19+
self === FileHandle.standardInput
20+
}
21+
}
22+
23+
// shell data handler protocol
24+
public protocol ShellDataHandler {
25+
26+
// called each time there is new data available
27+
func handle(_ data: Data)
28+
29+
// optional method called on the end of the execution process
30+
func end()
31+
}
32+
33+
public extension ShellDataHandler {
34+
35+
func end() {
36+
// default implementation: do nothing...
37+
}
38+
}
39+
40+
extension FileHandle: ShellDataHandler {
41+
42+
public func handle(_ data: Data) {
43+
self.write(data)
44+
}
45+
46+
public func end() {
47+
guard !self.isStandard else {
48+
return
49+
}
50+
self.closeFile()
51+
}
52+
}
53+
#endif
54+
55+
// a custom shell representation object
56+
open class Shell {
57+
58+
// shell errors
59+
public enum Error: LocalizedError {
60+
// invalid shell output data error
61+
case outputData
62+
// generic shell error, the first parameter is the error code, the second is the error message
63+
case generic(Int, String)
64+
65+
public var errorDescription: String? {
66+
switch self {
67+
case .outputData:
68+
return "Invalid or empty shell output."
69+
case .generic(let code, let message):
70+
return message + " (code: \(code))"
71+
}
72+
}
73+
}
74+
75+
// lock queue to keep data writes in sync
76+
private let lockQueue: DispatchQueue
77+
78+
// type of the shell, by default: /bin/sh
79+
public var type: String
80+
81+
// custom env variables exposed for the shell
82+
public var env: [String: String]
83+
84+
#if os(macOS)
85+
// output data handler
86+
public var outputHandler: ShellDataHandler?
87+
88+
// error data handler
89+
public var errorHandler: ShellDataHandler?
90+
#endif
91+
92+
/**
93+
Initializes a new Shell object
94+
95+
- Parameters:
96+
- type: The type of the shell, default: /bin/sh
97+
- env: Additional environment variables for the shell, default: empty
98+
99+
*/
100+
public init(_ type: String = "/bin/sh", env: [String: String] = [:]) {
101+
self.lockQueue = DispatchQueue(label: "shellkit.lock.queue")
102+
self.type = type
103+
self.env = env
104+
}
105+
106+
/**
107+
Runs a specific command through the current shell.
108+
109+
- Parameters:
110+
- command: The command to be executed
111+
112+
- Throws:
113+
`Shell.Error.outputData` if the command execution succeeded but the output is empty,
114+
otherwise `Shell.Error.generic(Int, String)` where the first parameter is the exit code,
115+
the second is the error message
116+
117+
- Returns: The output string of the command without trailing newlines
118+
*/
119+
@discardableResult
120+
public func run(_ command: String) throws -> String {
121+
let process = Process()
122+
process.launchPath = self.type
123+
process.arguments = ["-c", command]
124+
125+
if !self.env.isEmpty {
126+
process.environment = ProcessInfo.processInfo.environment
127+
self.env.forEach { variable in
128+
process.environment?[variable.key] = variable.value
129+
}
130+
}
131+
132+
var outputData = Data()
133+
let outputPipe = Pipe()
134+
process.standardOutput = outputPipe
135+
136+
var errorData = Data()
137+
let errorPipe = Pipe()
138+
process.standardError = errorPipe
139+
140+
#if os(macOS)
141+
outputPipe.fileHandleForReading.readabilityHandler = { handler in
142+
let data = handler.availableData
143+
self.lockQueue.async {
144+
outputData.append(data)
145+
self.outputHandler?.handle(data)
146+
}
147+
}
148+
errorPipe.fileHandleForReading.readabilityHandler = { handler in
149+
let data = handler.availableData
150+
self.lockQueue.async {
151+
errorData.append(data)
152+
self.errorHandler?.handle(data)
153+
}
154+
}
155+
#endif
156+
157+
process.launch()
158+
159+
#if os(Linux)
160+
self.lockQueue.sync {
161+
outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
162+
errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
163+
}
164+
#endif
165+
166+
process.waitUntilExit()
167+
168+
#if os(macOS)
169+
self.outputHandler?.end()
170+
self.errorHandler?.end()
171+
172+
outputPipe.fileHandleForReading.readabilityHandler = nil
173+
errorPipe.fileHandleForReading.readabilityHandler = nil
174+
#endif
175+
176+
return try self.lockQueue.sync {
177+
guard process.terminationStatus == 0 else {
178+
var message = "Unknown error"
179+
if let error = String(data: errorData, encoding: .utf8) {
180+
message = error.trimmingCharacters(in: .newlines)
181+
}
182+
throw Error.generic(Int(process.terminationStatus), message)
183+
}
184+
guard let output = String(data: outputData, encoding: .utf8) else {
185+
throw Error.outputData
186+
}
187+
return output.trimmingCharacters(in: .newlines)
188+
}
189+
}
190+
191+
/**
192+
Async version of the run command
193+
194+
- Parameters:
195+
- command: The command to be executed
196+
- completion: The completion block with the output and error
197+
198+
The command will be executed on a concurrent dispatch queue.
199+
*/
200+
public func run(_ command: String, completion: @escaping ((String?, Swift.Error?) -> Void)) {
201+
let queue = DispatchQueue(label: "shellkit.process.queue", attributes: .concurrent)
202+
queue.async {
203+
do {
204+
let output = try self.run(command)
205+
completion(output, nil)
206+
}
207+
catch {
208+
completion(nil, error)
209+
}
210+
}
211+
}
212+
}

Tests/LinuxMain.swift

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
LinuxMain.swift
3+
ShellKitTests
4+
5+
Created by Tibor Bödecs on 2018.12.31.
6+
Copyright Binary Birds. All rights reserved.
7+
*/
8+
9+
import XCTest
10+
@testable import ShellKitTests
11+
12+
XCTMain([
13+
testCase(ShellKitTests.allTests),
14+
])

0 commit comments

Comments
 (0)