Async output?
jeff-h opened this issue ยท 6 comments
I've skimmed the code and it appears you don't provide any way to get the output of a command as it comes through the pipe. Is this something you'd consider adding? I guess it would provide a "data received" callback argument or something?
I would love to see this added to ShellOut! ๐ It would also make async scripting with Marathon possible, which would be a big win. Would be happy to accept a PR adding this feature.
Got PR for this here
Edit, the original sounded a bit douchy in the second read. This PR seems to work well on Linux, is it possible to get it to work with Marathon? Thanks for the PR @rob-nash .
Glad someone finds it useful @lf-araujo no worries mate ๐
Any progress? It doesn't look like anyone made a progress update in years.
This is the closer way to a kind of workaround I found so far:
@discardableResult func shellOut(
to command: ShellOutCommand,
arguments: [String] = [],
at path: String = ".",
process: Process = .init(),
errorHandle: FileHandle? = nil,
liveOutput: @escaping (String) -> Void
) throws -> String {
let temporaryOutputURL = FileManager.default.temporaryDirectory.appendingPathComponent(
"shellout_live_output.temp"
)
if FileManager.default.fileExists(atPath: temporaryOutputURL.absoluteString) {
try FileManager.default.removeItem(at: temporaryOutputURL)
}
try Data().write(to: temporaryOutputURL)
let outputHandle = try FileHandle(forWritingTo: temporaryOutputURL)
#if DEBUG
print("To read live output file directly in a terminal")
print("tail -f \(temporaryOutputURL.path)")
#endif
outputHandle.waitForDataInBackgroundAndNotify()
let subscription = NotificationCenter.default.publisher(for: NSNotification.Name.NSFileHandleDataAvailable)
.tryReduce("", { alreadyDisplayedContent, _ in
let content = try String(contentsOf: temporaryOutputURL)
liveOutput(String(content[alreadyDisplayedContent.endIndex...]))
outputHandle.waitForDataInBackgroundAndNotify()
return content
})
.sink(receiveCompletion: {
switch $0 {
case let .failure(error):
print("Content of live output cannot be read: \(error)")
case .finished: break
}
}, receiveValue: { _ in })
let output = try shellOut(to: command, at: path, process: process, outputHandle: outputHandle, errorHandle: errorHandle)
subscription.cancel()
try FileManager.default.removeItem(at: temporaryOutputURL)
return output
}
Usage in my script:
try shellOut(
to: .iOSTest(
scheme: projectName,
simulatorName: device.simulatorName,
derivedDataPath: derivedDataPath,
testPlan: planName
)
) { print($0) } // I'm printing everything, but here you can filter what you really want to print out.
I know this is far from being nice, and there is certainly a way to do better.
I spent some times tinkering with outputHandle.readabilityHandler = { fileHandler in
where fileHandler.availableData
is always an empty data (0 bytes
) for some reasons. I guess fileHandler.availableData
is always empty because of a race condition, I certainly do something wrong about that.
So the best way I found, which looks over engineered, is to provide a file to shellOut
outputHandler
, use outputHandle.waitForDataInBackgroundAndNotify
on it, use some Combine
stuff to print the output...
If you have better approach, I'm all ears. I guess this PR will fix the issue once for all: #30. In the meanwhile, it's good to have a workaround for some scripts I guess.
(I also know that I'm ignoring the live error output, it's only because for my case, I don't need it for my case).