Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async output? #11

Open
jeff-h opened this issue Aug 30, 2017 · 6 comments · May be fixed by #30
Open

Async output? #11

jeff-h opened this issue Aug 30, 2017 · 6 comments · May be fixed by #30

Comments

@jeff-h
Copy link

jeff-h commented Aug 30, 2017

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?

@JohnSundell
Copy link
Owner

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.

@ghost
Copy link

ghost commented Apr 14, 2018

Got PR for this here

@lf-araujo
Copy link

lf-araujo commented Nov 1, 2018

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 .

@ghost
Copy link

ghost commented Nov 2, 2018

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

@ghost
Copy link

ghost commented Sep 30, 2020

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

@renaudjenny
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants