swift Process() return values

How do I access a returned value from a Process(), in this case 'which'...


  1. var sips_path : String?
  2. //MARK: locate sips on local machine
  3. let which_sips = Process()
  4. which_sips.executableURL = URL(fileURLWithPath: "which")
  5. which_sips.arguments = ["sips"]
  6. do { sips_path = try which_sips.run() }
  7. catch let error as NSError { sips_path = "/usr/bin/sips"; print("Failed to execute which_sips", error) }


line 8. gets compiler error "Cannot assign value of type '()' to type 'String?'"


I believe, but cannot prove, 'which' returns a string. .run() throws and throws are for errors only, right? So where is the result of calling which?

It seems I should use a closure to use $0 but it's already in one...


line 9. intends to assign a default path.

Answered by DTS Engineer in 688241022

it’s not obvious why you’re seeing the problem while I’m not.

So I dug deeper into this issue and have a status update…

To start, after looking into this in a lot more detail I’ve concluded the the final line in my code:

pipe.fileHandleForWriting.closeFile()

is, as eaigner suspected, incorrect. I still don’t know why it crashes for eaigner (and others!) but not for me, but I’m confident that it shouldn’t be there.

Anyway, I recently had cause to research this issue in a lot more depth, and I’ve put (what I hope is) my final answer in a new post, Running a Child Process with Standard Input and Output. Please read it through and post back here if you have further questions or comments.

Also, if you’re curious as to why I abandoned FileHandle here, see Whither FileHandle?.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Where did you get that run returns a string ?


AFAICT, run returns void.

Accepted Answer

The

which
command returns its result by printing
stdout
. The
run
method doesn’t return that; it actually returns nothing (
Void
) because the process executes asynchronously. To collect the output you have to do two things:
  • Wait for the process to finish

  • Collect its output

Doing this correctly is tricky for obscure low-level reasons. Pasted in below is some code I had lying around for this. If I run it like so:

try! launch(tool: URL(fileURLWithPath: "/usr/bin/which"), arguments: ["sips"]) { (status, outputData) in
    let output = String(data: outputData, encoding: .utf8) ?? ""
    print("done, status: \(status), output: \(output)")
}

it prints:

done, status: 0, output: /usr/bin/sips

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
func launch(tool: URL, arguments: [String], completionHandler: @escaping (Int32, Data) -> Void) throws {
    let group = DispatchGroup()
    let pipe = Pipe()
    var standardOutData = Data()

    group.enter()
    let proc = Process()
    proc.executableURL = tool
    proc.arguments = arguments
    proc.standardOutput = pipe.fileHandleForWriting
    proc.terminationHandler = { _ in
        proc.terminationHandler = nil
        group.leave()
    }

    group.enter()
    DispatchQueue.global().async {
        // Doing long-running synchronous I/O on a global concurrent queue block
        // is less than ideal, but I’ve convinced myself that it’s acceptable
        // given the target ‘market’ for this code.

        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        pipe.fileHandleForReading.closeFile()
        DispatchQueue.main.async {
            standardOutData = data
            group.leave()
        }
    }

    group.notify(queue: .main) {
        completionHandler(proc.terminationStatus, standardOutData)
    }

    try proc.run()

    // We have to close our reference to the write side of the pipe so that the
    // termination of the child process triggers EOF on the read side.

    pipe.fileHandleForWriting.closeFile()
}

Wow! Thanks Eskimo.


I intended to use 'which' to locate sips as a convenience if other OS's had it somewhere other than /usr/bin/ , which may not be true. I could just continue to stipulate that the user provide it there. Since 'which' itself must be in /usr/bin , I guess I'm OK with that stipulation.


Nevertheless, your answer taught me what I sought, and more. Should I execute a Process() in the future, that prints to stdout, I'll know how to do it.


Thanks again.

Someone asked me for an Objective-C version of the code I posted upthread and so here it is.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Code Block
static BOOL launchToolWithArgumentsError(
NSURL * _Nonnull url,
NSArray<NSString *> * _Nonnull arguments,
void (^completionHandler)(int status, NSData * _Nonnull outputData),
NSError * _Nullable * _Nullable errorPtr
) {
dispatch_group_t group = dispatch_group_create();
NSPipe * pipe = [NSPipe pipe];
NSMutableData * standardOutData = [NSMutableData data];
dispatch_group_enter(group);
NSTask * task = [[NSTask alloc] init];
task.executableURL = url;
task.arguments = arguments;
task.standardOutput = pipe.fileHandleForWriting;
task.terminationHandler = ^(NSTask * taskB) {
taskB.terminationHandler = nil;
dispatch_group_leave(group);
};
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
// Doing long-running synchronous I/O on a global concurrent queue block
// is less than ideal, but I’ve convinced myself that it’s acceptable
// given the target ‘market’ for this code.
NSData * newData = [pipe.fileHandleForReading readDataToEndOfFile];
[pipe.fileHandleForReading closeFile];
dispatch_async(dispatch_get_main_queue(), ^{
[standardOutData appendData:newData];
dispatch_group_leave(group);
});
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
completionHandler(task.terminationStatus, standardOutData);
});
BOOL result = [task launchAndReturnError:errorPtr];
// We have to close our reference to the write side of the pipe so that the
// termination of the child process triggers EOF on the read side.
[pipe.fileHandleForWriting closeFile];
return result;
}

So what's up with that last line?

What’s up with the last line is pretty well explained by the comment. I’m not sure why it’s crashing for you; the code I posted continues to work for me [1]. How are you testing this?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] On macOS 11.4, building with Xcode 12.5, and testing it like so:

let tool = URL(fileURLWithPath: "/bin/echo")
try! launch(tool: tool, arguments: ["Hello", "Cruel", "World!"]) { status, output in
    print(status, output)
}

Same issue for me

Crashed Thread:        24  Dispatch queue: com.apple.root.utility-qos

Exception Type:        EXC_CRASH (SIGABRT)
Exception Codes:       0x0000000000000000, 0x0000000000000000
Exception Note:        EXC_CORPSE_NOTIFY

Application Specific Information:
*** Terminating app due to uncaught exception 'NSFileHandleOperationException', reason: '*** -[NSConcreteFileHandle availableData]: Bad file descriptor'
terminating with uncaught exception of type NSException
abort() called

macOS 11.4 Xcode 12.5.1

Same issue for me

Weird. Your setup is almost identical to mine, so it’s not obvious why you’re seeing the problem while I’m not. My advice is that either you or eaigner open a DTS tech support incident so that I can allocate more time to dig into this.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Also I'm wondering why we are setting pipe.fileHandleForWriting in process output instead of pipe.fileHandleForReading in case if we just reading ?

Strings still return empty for me. If I do a search for files such as ls or which itself it returns them just fine. If I search for avr-gcc it returns empty as it has been doing for me all day. I can use "which avr-gcc" just fine from the terminal but in my swift code it returns nothing.

Edit: By that same token, it seems Whereis works for common programs in terminal but it returns nothing for apps like avr-gcc or anothers added via homebrew. I'm not sure the significance of that

it’s not obvious why you’re seeing the problem while I’m not.

So I dug deeper into this issue and have a status update…

To start, after looking into this in a lot more detail I’ve concluded the the final line in my code:

pipe.fileHandleForWriting.closeFile()

is, as eaigner suspected, incorrect. I still don’t know why it crashes for eaigner (and others!) but not for me, but I’m confident that it shouldn’t be there.

Anyway, I recently had cause to research this issue in a lot more depth, and I’ve put (what I hope is) my final answer in a new post, Running a Child Process with Standard Input and Output. Please read it through and post back here if you have further questions or comments.

Also, if you’re curious as to why I abandoned FileHandle here, see Whither FileHandle?.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

VitaliiKizlov wrote:

Also I'm wondering why we are setting pipe.fileHandleForWriting in process output instead of pipe.fileHandleForReading in case if we just reading ?

Yeah, that’s confusing. A pipe creates two file handles, one for reading and one for writing. You need to pass the one for writing to the child process’s stdout and then read its output via the one for reading.

Having said that, my new advice, as shown by the code in Running a Child Process with Standard Input and Output, is to pass the Pipe itself to Process, so it can pick up the correct file handle based on the direction of the I/O.


kelvin75 wrote:

Strings still return empty for me.

Let’s tackle this issue on the thread you created for it.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hey eskimo 🙂 Big thanks to your new solution 🙏 Maybe you can help me how to correctly add progress while child process is running ?

Maybe you can help me how to correctly add progress while child process is running ?

Sure. How about you start a new thread with the details? Use the same tags as this thread so that I see it go by.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

swift Process() return values
 
 
Q