ShellOut icon indicating copy to clipboard operation
ShellOut copied to clipboard

Async output?

Open jeff-h opened this issue 8 years ago • 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?

jeff-h avatar Aug 30 '17 09:08 jeff-h

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.

JohnSundell avatar Oct 20 '17 20:10 JohnSundell

Got PR for this here

ghost avatar Apr 14 '18 16:04 ghost

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 .

lf-araujo avatar Nov 01 '18 19:11 lf-araujo

Glad someone finds it useful @lf-araujo no worries mate 👍

ghost avatar Nov 02 '18 07:11 ghost

Any progress? It doesn't look like anyone made a progress update in years.

ghost avatar Sep 30 '20 21:09 ghost

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).

renaudjenny avatar Sep 05 '21 20:09 renaudjenny