Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c7b04d0
Bring back launchBashOldVersion for debugging
finestructure Aug 28, 2023
ea1adeb
Pass back both stdout and stderr
finestructure Aug 28, 2023
a4bebeb
Wire up old version
finestructure Aug 28, 2023
9cd139c
Apply Gwynne's proposed fix for empty readabilityHandler responses
finestructure Aug 29, 2023
49ad248
Gwynne's latest fixes
finestructure Aug 30, 2023
1818ff5
Don't forward devcontainer ports
finestructure Aug 30, 2023
480d845
Fix TSAN warning
finestructure Aug 30, 2023
4812163
Add shellout2 without the bash gymnastics
finestructure Aug 30, 2023
0a98c6a
Revert "Add shellout2 without the bash gymnastics"
finestructure Aug 30, 2023
d818288
Add swift-log dependency
finestructure Aug 31, 2023
3606d8a
Add stdout/stderr read logging
finestructure Aug 31, 2023
49fcdb6
Log command
finestructure Aug 31, 2023
a04e1e1
barrier sync changes
finestructure Aug 31, 2023
9b1179f
Explicitly flush available data (Linux doesn't signal)
finestructure Aug 31, 2023
8c8541a
Moar logs
finestructure Aug 31, 2023
e250b44
DispatchGroup version
finestructure Sep 1, 2023
e8cee8e
100ms timeout
finestructure Sep 1, 2023
d8c7bb5
Reset readabilityHandler
finestructure Sep 1, 2023
f851012
Reduce logging
finestructure Sep 1, 2023
3791236
Make eofTimeout configurable
finestructure Sep 1, 2023
826125c
Remove swift-log dependency
finestructure Sep 1, 2023
5077225
Use SocketPair
finestructure Sep 1, 2023
07fea99
Revert "Remove swift-log dependency"
finestructure Sep 1, 2023
0450bd9
Revert "Use SocketPair"
finestructure Sep 1, 2023
6571403
Change timeout warning to debug level
finestructure Sep 1, 2023
deba117
Reimplement launchBash() based on TSCBasic.Process, which is much mor…
gwynne Sep 1, 2023
27af7ad
Remove old version
finestructure Sep 2, 2023
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
3 changes: 1 addition & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,5 @@
}
}
}
},
"forwardPorts": [8080]
}
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/Packages
/*.xcodeproj
.swiftpm
.vscode/
69 changes: 56 additions & 13 deletions Package.resolved

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

14 changes: 11 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:4.2
// swift-tools-version:5.8

/**
* ShellOut
Expand All @@ -10,23 +10,31 @@ import PackageDescription

let package = Package(
name: "ShellOut",
platforms: [.macOS("10.15.4")],
products: [
.library(name: "ShellOut", targets: ["ShellOut"])
],
dependencies: [
.package(url: "https://github.com/SwiftPackageIndex/ShellQuote", from: "1.0.2"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-tools-support-core.git", from: "0.5.2"),
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),
],
targets: [
.target(
name: "ShellOut",
dependencies: [
.product(name: "ShellQuote", package: "ShellQuote")
.product(name: "ShellQuote", package: "ShellQuote"),
.product(name: "Logging", package: "swift-log"),
.product(name: "Algorithms", package: "swift-algorithms"),
.product(name: "TSCBasic", package: "swift-tools-support-core"),
],
path: "Sources"
),
.testTarget(
name: "ShellOutTests",
dependencies: ["ShellOut"]
dependencies: ["ShellOut"],
exclude: ["Fixtures"]
)
]
)
150 changes: 58 additions & 92 deletions Sources/ShellOut.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

import Foundation
import Dispatch
import Logging
import TSCBasic
import Algorithms

// MARK: - API

Expand All @@ -32,18 +35,23 @@ import Dispatch
to command: SafeString,
arguments: [Argument] = [],
at path: String = ".",
process: Process = .init(),
logger: Logger? = nil,
outputHandle: FileHandle? = nil,
errorHandle: FileHandle? = nil,
environment: [String : String]? = nil
) throws -> String {
let command = "cd \(path.escapingSpaces) && \(command) \(arguments.map(\.string).joined(separator: " "))"
) async throws -> (stdout: String, stderr: String) {
let command = "\(command) \(arguments.map(\.string).joined(separator: " "))"

return try process.launchBash(
return try await TSCBasic.Process.launchBash(
with: command,
logger: logger,
outputHandle: outputHandle,
errorHandle: errorHandle,
environment: environment
environment: environment,
at: path == "." ? nil :
(path == "~" ? TSCBasic.localFileSystem.homeDirectory.pathString :
(path.starts(with: "~/") ? "\(TSCBasic.localFileSystem.homeDirectory.pathString)/\(path.dropFirst(2))" :
path))
)
}

Expand All @@ -68,16 +76,16 @@ import Dispatch
@discardableResult public func shellOut(
to command: ShellOutCommand,
at path: String = ".",
process: Process = .init(),
logger: Logger? = nil,
outputHandle: FileHandle? = nil,
errorHandle: FileHandle? = nil,
environment: [String : String]? = nil
) throws -> String {
try shellOut(
) async throws -> (stdout: String, stderr: String) {
try await shellOut(
to: command.command,
arguments: command.arguments,
at: path,
process: process,
logger: logger,
outputHandle: outputHandle,
errorHandle: errorHandle,
environment: environment
Expand Down Expand Up @@ -390,69 +398,49 @@ extension ShellOutCommand {

// MARK: - Private

private extension Process {
@discardableResult func launchBash(with command: String, outputHandle: FileHandle? = nil, errorHandle: FileHandle? = nil, environment: [String : String]? = nil) throws -> String {
executableURL = URL(fileURLWithPath: "/bin/bash")
arguments = ["-c", command]

if let environment = environment {
self.environment = environment
}

// Because FileHandle's readabilityHandler might be called from a
// different queue from the calling queue, avoid a data race by
// protecting reads and writes to outputData and errorData on
// a single dispatch queue.
let outputQueue = DispatchQueue(label: "bash-output-queue")

var outputData = Data()
var errorData = Data()

let outputPipe = Pipe()
standardOutput = outputPipe

let errorPipe = Pipe()
standardError = errorPipe

outputPipe.fileHandleForReading.readabilityHandler = { handler in
let data = handler.availableData
outputQueue.async {
outputData.append(data)
outputHandle?.write(data)
}
}

errorPipe.fileHandleForReading.readabilityHandler = { handler in
let data = handler.availableData
outputQueue.async {
errorData.append(data)
errorHandle?.write(data)
}
}

try run()

waitUntilExit()

outputHandle?.closeFile()
errorHandle?.closeFile()

outputPipe.fileHandleForReading.readabilityHandler = nil
errorPipe.fileHandleForReading.readabilityHandler = nil

// Block until all writes have occurred to outputData and errorData,
// and then read the data back out.
return try outputQueue.sync {
if terminationStatus != 0 {
throw ShellOutError(
terminationStatus: terminationStatus,
errorData: errorData,
outputData: outputData
)
private extension TSCBasic.Process {
@discardableResult static func launchBash(
with command: String,
logger: Logger? = nil,
outputHandle: FileHandle? = nil,
errorHandle: FileHandle? = nil,
environment: [String : String]? = nil,
at: String? = nil
) async throws -> (stdout: String, stderr: String) {
let process = try Self.init(
arguments: ["/bin/bash", "-c", command],
environment: environment ?? ProcessEnv.vars,
workingDirectory: at.map { try .init(validating: $0) } ?? TSCBasic.localFileSystem.currentWorkingDirectory ?? .root,
outputRedirection: .collect(redirectStderr: false),
startNewProcessGroup: false,
loggingHandler: nil
)

try process.launch()

let result = try await process.waitUntilExit()

try outputHandle?.write(contentsOf: (try? result.output.get()) ?? [])
try outputHandle?.close()
try errorHandle?.write(contentsOf: (try? result.stderrOutput.get()) ?? [])
try errorHandle?.close()

guard case .terminated(code: let code) = result.exitStatus, code == 0 else {
let code: Int32
switch result.exitStatus {
case .terminated(code: let termCode): code = termCode
case .signalled(signal: let sigNo): code = -sigNo
}

return outputData.shellOutput()
throw ShellOutError(
terminationStatus: code,
errorData: Data((try? result.stderrOutput.get()) ?? []),
outputData: Data((try? result.output.get()) ?? [])
)
}
return try (
stdout: String(result.utf8Output().trimmingSuffix(while: \.isNewline)),
stderr: String(result.utf8stderrOutput().trimmingSuffix(while: \.isNewline))
)
}
}

Expand All @@ -468,25 +456,3 @@ private extension Data {

}
}

private extension String {
var escapingSpaces: String {
return replacingOccurrences(of: " ", with: "\\ ")
}

func appending(argument: String) -> String {
return "\(self) \"\(argument)\""
}

func appending(arguments: [String]) -> String {
return appending(argument: arguments.joined(separator: "\" \""))
}

mutating func append(argument: String) {
self = appending(argument: argument)
}

mutating func append(arguments: [String]) {
self = appending(arguments: arguments)
}
}
Binary file added Tests/ShellOutTests/Fixtures/ErrNo.zip
Binary file not shown.
Loading