macoscocoanstasknspipe

Multiple terminal commands using NSTask at once


I need to run the following command from an OSX app:

dscl . -read /Users/user JPEGPhoto | tail -1 | xxd -r -p > /Users/user/Desktop/user.jpg

Ive tried several things such as:

func runScript(launchPath:String, scriptName:String) {

    let task = NSTask()
    task.launchPath = launchPath
    task.arguments = NSArray(objects: scriptName)

    let pipe = NSPipe()
    task.standardOutput = pipe
    task.launch()

    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output: String = NSString(data: data, encoding: NSUTF8StringEncoding)

}

and

func runCommand(command: String) -> (output: String, exitStatus: Int) {
    let tokens = command.componentsSeparatedByString(" ")
    let launchPath = tokens[0]
    let arguments = tokens[1..<tokens.count]

    let task = NSTask()
    task.launchPath = launchPath
    task.arguments = Array(arguments)
    let stdout = NSPipe()
    task.standardOutput = stdout

    task.launch()
    task.waitUntilExit()

    let outData = stdout.fileHandleForReading.readDataToEndOfFile()
    let outStr = NSString(data: outData, encoding: NSUTF8StringEncoding)
    return (outStr, Int(task.terminationStatus))
}

The problem is that these methods execute one command per call, so I would have to call them three times (dscl/tail/xxd) which doesn't work.

When I tried them separately in terminal, it didn't work either.

Any suggestions? Thanks

UPDATE:

After following Ken Thomases' great advice, this is what it looks like in swift:

import Collaboration
func saveUserPicture() {
    var userImage:NSImage = CBUserIdentity(posixUID: getuid(), authority: CBIdentityAuthority.defaultIdentityAuthority()).image() as NSImage
    var userImageData:NSData = NSBitmapImageRep.representationOfImageRepsInArray(userImage.representations, usingType: NSBitmapImageFileType.NSJPEGFileType, properties: nil )
    userImageData.writeToFile("/Users/user/Desktop/file.jpg", atomically: true)
}

Solution

  • Are you sure you need to run that command and use NSTask? That information should be available through direct APIs.

    In particular, I think something like the following should get you the image for a user:

    NSImage* image = [[CBUserIdentity identityWithName:@"user" authority:[CBIdentityAuthority defaultIdentityAuthority]] image];
    

    You could then save it as a file if you really want to:

    NSData* data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType:NSJPEGFileType properties:nil];
    [data writeToURL:someURL atomically:YES];
    

    As per the question you actually asked, you need to use one NSTask per command. You create an NSPipe for each pipe (|) in the shell command you're emulating. You set one pipe as the output of the first task and the input of the second. You set the other pipe as the output of the second and the input of the first. You then run all three tasks and wait for the last to complete.

    If you want, you can use another pipe for the standard error of each task and/or the output of the last task. Be sure to read from such pipes asynchronously simultaneously with waiting for the last task to exit (or, even better, don't block waiting for the last task but go back to the event loop and let the task notify you when it completes).

    The arguments for each task should be an array. If you're going to try to run these tasks instead of using the proper API, you would ideally always have the command as an array and not a string. Parsing the string is a pain to get right, depending on which features of a shell you want to emulate. So, for the first task, the launch path would be @"dscl" and the arguments would be @[ @".", @"-read", @"/Users/user", @"JPEGPhoto" ]. Similarly for the other tasks.

    Just for completeness, I'll say that you can run a full command line like your original string by enlisting the shell to parse it and run subprocesses for each of the subcommands. Set the launch path to @"/bin/sh" and the arguments to @[ @"-c", @"dscl . -read /Users/user JPEGPhoto | tail -1 | xxd -r -p > /Users/user/Desktop/user.jpg" ]. I really don't recommend this, though. Assuming the real command that you want to run is dynamic and you'll be constructing the command line programmatically, it's easy to create problems. If you build a string that includes characters that the shell treats specially, you'll get unexpected results and possibly even do something dangerous. Also, personally, it just feels wrong to build a string just so that the shell can pick it apart when you could do the original work yourself directly.