NSTask and Standard Output

I’m working to learn NSTask, and I’ve gotten the basics to work, but I’m stuck on one point. How can I set a variable to standard output? I’ve been working with the following script just for test purposes.

BTW, the standard output will be text, a random example of which might be a command-line utility that returns the number of pages in a PDF. Thanks for the help.

use framework "Foundation"

set theTask to (current application's NSTask's new())
theTask's setLaunchPath:"/bin/echo"
theTask's setArguments:{"OK"}
theTask's launchAndReturnError:(missing value)
set theOutput to theTask's standardOutput()

theOutput --> «class ocid» id «data optr000000006060870300600000»

You can find code here:

https://forum.latenightsw.com/t/applescriptobjc-and-do-shell-script/2069

Thanks Shane. I read through your script, which is at or beyond the boundary of my knowledge level, and it’s clear that doing what I want is not going to happen right now. I’ll use do shell script in those instances, which is fine.

In some cases, I only need to ascertain if a particular command completed successfully but even that doesn’t seem to work. For example, the first of the following scripts succeeds and the second fails but they both return the same results.

set theFolder to POSIX path of (path to desktop as text) & "FolderDoesNotExist/NewFolder"
set theTask to (current application's NSTask's new())
theTask's setLaunchPath:"/bin/mkdir"
theTask's setArguments:{"-p", theFolder}
set {theResult, theError} to theTask's launchAndReturnError:(reference) --> {true, missing value}

set theFolder to POSIX path of (path to desktop as text) & "FolderDoesNotExist/NewFolder"
set theTask to (current application's NSTask's new())
theTask's setLaunchPath:"/bin/mkdir"
theTask's setArguments:{theFolder}
set {theResult, theError} to theTask's launchAndReturnError:(reference) --> {true, missing value}

The launchAndReturnError: method only returns an error if the task can’t be launched.

Thanks Shane and Fredrik71 for the explanation, which makes perfect sense.

I spent some time studying Shane’s script and carefully read the paragraphs that introduce the script, and I now have a pretty good idea of what’s required to use NSTask in place of do shell script. Given the simple stuff I do, NSTask will not be as useful as I’d hoped, but I learned a lot, which is always good.

@peavine

I found this code snippet I wrote as an exercise 3 or 4 years ago, buried in my library of code snippets, that I kept as they might be useful in future projects.


use AppleScript version "2.4"
use scripting additions
use framework "Foundation"

property myApp : a reference to current application

set shellTask to myApp's NSTask's new()
shellTask's setLaunchPath:"/bin/bash/"
shellTask's setArguments:{"-c", "whereis osascript"}

set shellOutputPipe to myApp's NSPipe's pipe()
set shellErrorPipe to myApp's NSPipe's pipe()

shellTask's setStandardOutput:shellOutputPipe
shellTask's setStandardError:shellErrorPipe

shellTask's |launch|()
shellTask's waitUntilExit()

if not (shellTask's isRunning() as boolean) then
	set shellTaskStatus to shellTask's terminationStatus() as integer
	if shellTaskStatus = 0 then -- Task Succeeded
		set shellTaskOutputHandle to shellOutputPipe's fileHandleForReading()
		set shellTaskOutputData to shellTaskOutputHandle's readDataToEndOfFile()
		set shellTaskOutputString to myApp's NSString's alloc()'s initWithData:shellTaskOutputData encoding:(myApp's NSUTF8StringEncoding)
		return shellTaskOutputString as text
	else if shellTaskStatus = 1 then -- Task Failed
		set shellTaskErrorHandle to shellErrorPipe's fileHandleForReading()
		set shellTaskErrorData to shellTaskErrorHandle's readDataToEndOfFile()
		set shellTaskErrorString to myApp's NSString's alloc()'s initWithData:shellTaskErrorData encoding:(myApp's NSUTF8StringEncoding)
		set shellTaskErrorReason to shellTask's terminationReason() as integer
		if shellTaskErrorReason = 1 then -- Task Failed Because of an Empty String Result, Or Has Error String Result
			return shellTaskErrorString as text
		else if shellTaskErrorReason = 2 then -- Task Properly Failed to Complete Execution for Unknown Reason, Maybe No Error String Result
			return "Task Properly Failed to Complete Execution for Unknown Reason" as text
		end if
	end if
end if

I’m not suggesting it is a definitive way of using the NSTask class, as Shane would have a lot more experience with this stuff, but it certainly worked ok back on Sierra.
I know a lot of the NSTask function’s where marked for deprecation some time ago, so it may not work on the later MacOS’s.

The “if not (shellTask’s isRunning() as boolean) then” line can be eliminated, as the code execution waits at “shellTask’s waitUntilExit()” for the task to complete anyway, but I included it just for completeness, and to show all of the possibilities.

The “shellTask’s terminationReason()” and if clauses can also be excluded most of the time, but they Highlight the difference between an Error message failure, and a massive failure by the NSTask object itself.


-- If you change the  line.

shellTask's setArguments:{"-c", "whereis osascript"}

--TO

shellTask's setArguments:{"-c", "whereis supermodels"}


You will see what I mean, as the error is merely an empty string, which is the same result you would get in the Terminal. because supermodels are not installed on the system unfortunately.
But you may well want to check for the empty string, even though it was sent to the StandardError pipe.
So it is an error, but not a failure error.

Anyway it might be useful stuff for you, or maybe not.
But Good Luck with it.

Regards Mark

Mark,

Relying on waitUntilExit works fine – as long as the task doesn’t return more data than can be buffered. The repeat loop testing for isRunning in my script allows the output to be collected periodically, avoiding overflows (and hence lost output).

Thanks Mark. Just to better understand your script, I used my example from post 1 and deleted everything but the bare essentials. I also changed launch to launchAndReturnError. The script worked exactly as expected. I also tested it with a utility that can get the number of pages in a PDF and it also worked well. In these instances, of course, its probably best just to use do shell script but it’s good to have this as an alternative. My edited version of your script doesn’t return standard error but perhaps that would not be needed in every instance.

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"

property myApp : a reference to current application

set shellTask to myApp's NSTask's new()
shellTask's setLaunchPath:"/bin/echo"
shellTask's setArguments:{"-n", "OK"} -- the -n option removes trailing linefeed

set shellOutputPipe to myApp's NSPipe's pipe()
shellTask's setStandardOutput:shellOutputPipe

set {theResult, theError} to shellTask's launchAndReturnError:(reference)
shellTask's waitUntilExit()
if theResult as boolean = false then return "The echo command failed"

set shellTaskOutputHandle to shellOutputPipe's fileHandleForReading()
set shellTaskOutputData to shellTaskOutputHandle's readDataToEndOfFile()
set shellTaskOutputString to myApp's NSString's alloc()'s initWithData:shellTaskOutputData encoding:(myApp's NSUTF8StringEncoding)

if shellTaskOutputString as text = "" then
	return "The returned string was empty"
else
	return shellTaskOutputString as text
end if

Shane

You’re absolutely right, you wouldn’t use waitUntilExit() for time consuming tasks with a lot of data, one because it would obviously block the script for a long time, and two it could possibly cause data loss.
And you would obviously not need to use both, you would use either waitUntilExit() or isRunning in a repeat loop, depending on the type of task and and the expected data load.

My code snippet library is a lot of useful bits of code, rather than complete solution’s, a lot of them are OTT examples, showing many of the possible options, where you might just use bits of the code, or indeed none of it.
I thought the OP might find bits in their useful, and show some alternative ways of using the NSTask class.

The terminationReason() part is not always required, but does show how standardError output might be expected to contain a useful result string, and not just errors.

Regards Mark

@peavine

My posting was not suppose to be a complete solution, I posted it to show some alternative ways of using the NSTask class, as you’ve discovered, a mixture of your’s and mine can work equally as well.

Using NSTask in AppleScript has it’s place in certain circumstances, but I often just use “do shell script” myself, because it’s generally good enough for the job.
I suspect “do shell script” possibly uses NSTask’s “launchAndReturnError:” behind the scenes.

I posted the code snippet to highlight that standardError can often contain a useful and sometimes expected result, especially when looking for a supermodel installation on your system, as the empty string result tells you that there not installed, and is not just spitting out an error message.

You may have noticed that the NSTask class has a number of pending deprecations planned, this includes launchPath(), and launch(), so it’s possible that my code won’t run in future OS releases.
I’ve seen these same deprecation’s are listed in the NSTask Swift equivalent Process() class as well.
And have read a lot recently about Apple encouraging developers to use XPC Services in there app’s, instead of NSTask() and Process().
So this could be a sign of the future for the NSTask class.

Regards Mark

Just for learning purposes (not for actual use), I modified my earlier edit of Marks’s script to create a script that returns standard error. If the script is unable to run the command (mkdir in this case), then an error message to that effect is returned. If the mkdir command runs but returns an error message, then that error message is returned by the script. I’ll probably seldom if ever use this, but it’s good to know the basics of how something works.

use framework "Foundation"
use scripting additions

set shellTask to current application's NSTask's new()
set shellErrorPipe to current application's NSPipe's pipe()

shellTask's setLaunchPath:"/bin/mkdir"
shellTask's setArguments:{"/Users/Robert/DoesNotExist/New Folder"}
shellTask's setStandardError:shellErrorPipe
set {theResult, theError} to shellTask's launchAndReturnError:(reference)
shellTask's waitUntilExit()
if theResult as boolean = false then return "The mkdir command failed"

set shellTaskErrorHandle to shellErrorPipe's fileHandleForReading()
set shellTaskErrorData to shellTaskErrorHandle's readDataToEndOfFile()
set shellTaskErrorString to current application's NSString's alloc()'s initWithData:shellTaskErrorData encoding:(current application's NSUTF8StringEncoding)
return shellTaskErrorString as text --> "mkdir: /Users/Robert/DoesNotExist: No such file or directory"

@peavine

Yeah I think sometimes folks could just use some ideas or options, rather than complete solutions pushed on them, especially when there are a number of different ways to achieve the same result.
In the case of running shell script’s, there are a few option’s for AppleScripter’s with the “do shell script” command, and the various ways of using “NSTask”, your choice would be based on the type of shell command and data load.

I’m with you, I think it’s good to know how things work thoroughly sometimes, in order give you the knowledge for future projects and solutions.
And a lot of people can generally figure out what they want, when they understand all of the options available to them.

Good luck with it.

Regards Mark