I am trying to run a simple shell command that runs and returns text rather quickly inside of a loop of indeterminate size at compile time that is generated by an NSArray. In scripting languages like perl, I would be able to do something like this:
for(i=0;i<=$myinputarraysize;i++){
$output[i]=`/my/task $inputarray[i]`;
}
This would build a new array for me from the expected output of my task. In Obj-C this seems to be much more difficult and a bit confusing to me. Right now my loop looks like this:
for(int i=0; i<[inputarray count]; i++){
NSTask *task;
task = [[NSTask alloc] init];
[task setLaunchPath:nsdchat];
NSArray *args;
args = [NSArray arrayWithObjects:#"/my/task", [inputarray objectAtIndex:i], nil];
[task setArguments:args];
NSPipe *pipe;
pipe = [NSPipe pipe];
[task setStandardOutput:pipe];
NSFileHandle *file;
file = [pipe fileHandleForReading];
[task launch];
NSData *data;
data = [file readDataToEndOfFile];
NSString *desc;
desc = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
desc = [string stringByReplacingOccurrencesOfString:#"\n" withString:#""];
[descriptions insertObject:desc atIndex:i];
[task release];
[args release];
[pipe release];
[file release];
[data release];
}
My goal is to fill descriptions (an NSMutableArray) with the output from my task (which I know is always a string and always ends in a newline that I want to strip out). It seems I'm missing something about memory releasing as when I run this and NSLog the output, I get the same result for the entire count of the loop.
Is there any easier or more way to loop through simple tasks like this? Am I overcomplicating it for myself?
First of all, there's no need to do most of that work inside the loop. task, pipe and file all look like they could be handled outside the loop. You should also consider using Objective-C 2.0 dot syntax and fast enumeration to cut things down a little.
More significantly:
NSArray *args;
args = [NSArray arrayWithObjects:#"/my/task", [inputarray objectAtIndex:i], nil];
[task setArguments:args];
This says that the first argument passed to the executable at path ndschat is /my/task. Which doesn't seem to match your PERL usage. Probably you want just:
NSArray *args;
args = [NSArray arrayWithObject:[inputarray objectAtIndex:i]];
[task setArguments:args];
Or, with the style comments taken into account:
for(NSString *argument in inputarray)
{
...
task.arguments = [NSArray arrayWithObject:argument];
...
}
EDIT: you're also releasing a lot of objects you don't own, which as well as adding heft to your code is a memory management error possibly leading to a crash. So, to cut the whole thing down and correct that fault:
for(NSString *argument in inputarray)
{
NSTask *task = [[NSTask alloc] init]; // you now own this
task.launchPath = nsdchat;
NSPipe *pipe = [NSPipe pipe]; // you don't own this
task.standardOutput = pipe;
NSFileHandle *file = [pipe fileHandleForReading]; // you also don't own this
task.arguments = [NSArray arrayWithObject:argument];
[task launch];
[task waitUntilExit]; // you should wait until the task is done
NSData *data = [file readDataToEndOfFile]; // this is another thing
// you don't own. Note also that
// readDataToEndOfFile advances
// the current read pointer, so
// it should be fine to do this
// successively
NSString *desc;
desc = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]
autorelease];
// you don't own this because
// of the autorelease; you
// don't want to own it since
// the next line will throw
// it away
desc = [string stringByReplacingOccurrencesOfString:#"\n" withString:#""];
// you don't own this
[descriptions addObject:desc];
[task release];
}
Related
How can I programmatically run terminal command?
Now I'm doing like this:
-(IBAction)proceedTapped:(id)sender
{
NSLog(#"%#",[self runCommand:#"cd ~"]);
NSLog(#"%#",[self runCommand:#"ls"]);
}
-(NSString *)runCommand:(NSString *)commandToRun
{
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath: #"/bin/sh"];
NSArray *arguments = [NSArray arrayWithObjects:
#"-c" ,
[NSString stringWithFormat:#"%#", commandToRun],
nil];
[task setArguments: arguments];
NSPipe *pipe;
pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
NSFileHandle *file;
file = [pipe fileHandleForReading];
[task launch];
NSData *data;
data = [file readDataToEndOfFile];
NSString *output;
output = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
return output;
}
For single commands this code works well, but in my case, when I want to change directory
to ~(home) and then do 'ls' this doesn't work well (it looks like to run two commands in two different terminal windows).
So how to run multiple commands like in one terminal window ?
Thank you.
In one of my applications I have:-
- (IBAction)openDirInTerminal:(id)sender { // context only
DirectoryItem *item = (DirectoryItem *)[self.dirTree focusedItem];
NSString *s = [NSString stringWithFormat:
#"tell application \"Terminal\" to do script \"cd \'%#\'\"", item.fullPath];
NSAppleScript *as = [[NSAppleScript alloc] initWithSource: s];
[as executeAndReturnError:nil];
}
DirectoryItem *item = (DirectoryItem *)[self.dirTree focusedItem]; is my method, but replace item.fullPath with a Path.
You could add the 2nd command to the script string (followed by a newline).
In my program I currently use NSTask 5 times, and it all works very well, but I'm tired of having to repeat so much code when it's all so similar, so I tried putting it in a function. Unfortunately it results in a crash on the line: [task launch]. Other than that I can't figure out what's causing the crash as if I use this code outside the function it works perfectly.
The method I am using is as follows:
- (NSString *)performTask: (NSString *)launchPath: (NSString *)argument1: (NSString *)argument2: (NSString *)argument3: (NSString *)argument4: (NSString *)argument5
{
NSString *resPath = [[NSBundle mainBundle] resourcePath];
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath: [NSString stringWithFormat: launchPath, resPath]];
NSArray *arguments = [NSArray arrayWithObjects: argument1, argument2, argument3, argument4, argument5, nil];
[task setArguments: arguments];
NSPipe *pipe = [NSPipe pipe];
[task setStandardInput:[NSPipe pipe]];
[task setStandardOutput: pipe];
NSFileHandle *file = [pipe fileHandleForReading];
[task launch];
NSData *data = [file readDataToEndOfFile];
NSString *status = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
return status;
}
I really hope this can be fixed, I really cannot see why this crashes.
Thanks in advance everyone.
Check this out it's pretty cool I use it:
https://gist.github.com/1875386
It's also much easier than using arrayWithObjects: for NSTask...
rc
I'm trying to use pipes to handle a command that requires multiple inputs, but not quite sure how to do it. Here's a snippet of what I'm trying to do. I know how to handle the first input, but I'm at lost as to piping in the second input -newstdinpass
NSTask *task = [[NSTask alloc] init];
NSPipe *pipe = [NSPipe pipe];
[task setLaunchPath: #"/bin/sh"];
[task setArguments: [NSArray arrayWithObjects: #"-c", #"/usr/bin/hdiutil chpass -oldstdinpass -newstdinpass /path/to/dmg", nil]];
[task setStandardInput:pipe];
[task launch];
[[pipe fileHandleForWriting] writeData:[#"thepassword" dataUsingEncoding:NSUTF8StringEncoding]];
[[pipe fileHandleForWriting] closeFile];
[task waitUntilExit];
[task release];
So I know using hdiutil in this manner is a bit of a hack, but in terms of pipes, am I going about it the correct way?
Thanks.
UPDATE: In case others are wondering about this, a quick solution to my problem is to pass in a null-terminated string as Ken Thomases had pointed out below. Use [[NSString stringWithFormat:#"oldpass\0newpass\0"] dataUsingEncoding:NSUTF8StringEncoding] into the pipe. Now, still need to learn how to bridge multiple NSTasks with pipes...
You can create multiple NSTasks and a bunch of NSPipes and hook them together, or you can use the sh -c trick to feed a shell a command, and let it parse it and set up all the IPC.
Example :
NSTask *task;
task = [[NSTask alloc] init];
[task setLaunchPath: #"/bin/sh"];
NSArray *arguments;
arguments = [NSArray arrayWithObjects: #"-c",
#"cat /usr/share/dict/words | grep -i ham | rev | tail -5", nil];
[task setArguments: arguments];
// and then do all the other jazz for running an NSTask.
Reference : http://borkware.com/quickies/one?topic=nstask
Now, as for a "proper" command execution function, here's one I'm usually using...
Code :
/*******************************************************
*
* MAIN ROUTINE
*
*******************************************************/
- (void)runCommand:(NSString *)cmd withArgs:(NSArray *)argsArray
{
//-------------------------------
// Set up Task
//-------------------------------
if (task) { [self terminate]; }
task = [[NSTask alloc] init];
[task setLaunchPath:cmd];
[task setArguments:argsArray];
[task setStandardOutput:[NSPipe pipe]];
[task setStandardError:[task standardOutput]];
//-------------------------------
// Set up Observers
//-------------------------------
[PP_NOTIFIER removeObserver:self];
[PP_NOTIFIER addObserver:self
selector:#selector(commandSentData:)
name: NSFileHandleReadCompletionNotification
object: [[task standardOutput] fileHandleForReading]];
[PP_NOTIFIER addObserver:self
selector:#selector(taskTerminated:)
name:NSTaskDidTerminateNotification
object:nil];
//-------------------------------
// Launch
//-------------------------------
[[[task standardOutput] fileHandleForReading] readInBackgroundAndNotify];
[task launch];
}
/*******************************************************
*
* OBSERVERS
*
*******************************************************/
- (void)commandSentData:(NSNotification*)n
{
NSData* d;
d = [[n userInfo] valueForKey:NSFileHandleNotificationDataItem];
if ([d length])
{
NSString* s = [[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding];
NSLog(#"Received : %#",s);
}
[[n object] readInBackgroundAndNotify];
}
- (void)taskTerminated:(NSNotification*)n
{
[task release];
task = nil;
}
Your use of the pipe looks correct to me.
I'm not sure why you're using /bin/sh. Just set up the NSTask with its launch path being #"/usr/bin/hdiutil" and with its arguments being an array of #"chpass", #"-oldstdinpass", #"-newstdinpass", and #"/path/to/dmg". This is much safer. For example, what if the path to the dmg contains a character the shell would interpret, like $?
Unless you specifically want to take advantage of a shell feature, don't use the shell.
I'm trying to call ffmpeg from NSTask in objective-c. I execute the ffmpeg command in terminal and it works flawlessly every time. I make the same command using NSTask, and it never gives me the whole output. It cuts it off half way through the output, at a seemingly random spot every time. Here is my code.
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSString* ffmpegPath = [[NSBundle mainBundle] pathForResource:#"ffmpeg" ofType:#""];
NSString* path = #"test.mov";
NSTask *task = [[NSTask alloc] init];
NSArray *arguments = [NSArray arrayWithObjects: #"-i", path, nil];
NSPipe *pipe = [NSPipe pipe];
NSFileHandle * read = [pipe fileHandleForReading];
[task setLaunchPath: ffmpegPath];
[task setArguments: arguments];
[task setStandardOutput: pipe];
[task launch];
[task waitUntilExit];
NSData* data = [read readDataToEndOfFile];
NSString* stringOutput = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"%#", stringOutput);
NSLog(#"%i", [task terminationStatus]);
NSLog(#"DONE");
}
And just like that I figured it out. Apparently the output had non UTF8Characters in it. Switched it over to NSASCIIStringEncoding and voila. Magic.
Here is my code:
task = [[NSTask alloc] init];
[task setCurrentDirectoryPath:#"/applications/jarvis/brain/"];
[task setLaunchPath:#"/applications/jarvis/brain/server.sh"];
NSPipe * out = [NSPipe pipe];
[task setStandardOutput:out];
[task launch];
[task waitUntilExit];
[task release];
NSFileHandle * read = [out fileHandleForReading];
NSData * dataRead = [read readDataToEndOfFile];
NSString * stringRead = [[[NSString alloc] initWithData:dataRead encoding:NSUTF8StringEncoding] autorelease];
So I'm trying to replicate this:
cd /applications/jarvis/brain/
./server.sh
but using NSTask in objective-c.
For some reason though, when I run this code, stringRead, returns nothing. It should return what terminal is returning when I launch the .sh file. Correct?
Any ideas?
Elijah
Xcode Bug
There's a bug in Xcode that stops it from printing any output after a a new task using standard output is launched (it collects all output, but no longer prints anything). You're going to have to call [task setStandardInput:[NSPipe pipe]] to get it to show output again (or, alternatively, have the task print to stderr instead of stdout).
Suggestion for final code:
NSTask *server = [NSTask new];
[server setLaunchPath:#"/bin/sh"];
[server setArguments:[NSArray arrayWithObject:#"/path/to/server.sh"]];
[server setCurrentDirectoryPath:#"/path/to/current/directory/"];
NSPipe *outputPipe = [NSPipe pipe];
[server setStandardInput:[NSPipe pipe]];
[server setStandardOutput:outputPipe];
[server launch];
[server waitUntilExit]; // Alternatively, make it asynchronous.
[server release];
NSData *outputData = [[outputPipe fileHandleForReading] readDataToEndOfFile];
NSString *outputString = [[[NSString alloc] initWithData:outputData encoding:NSUTF8StringEncoding] autorelease]; // Autorelease optional, depending on usage.
The solution above is freezing because it's synchronous.
Call to [server waitUntilExit] blocks the run loop until the tasks is done.
Here's the async solution for getting the task output.
task.standardOutput = [NSPipe pipe];
[[task.standardOutput fileHandleForReading] setReadabilityHandler:^(NSFileHandle *file) {
NSData *data = [file availableData]; // this will read to EOF, so call only once
NSLog(#"Task output! %#", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
// if you're collecting the whole output of a task, you may store it on a property
[self.taskOutput appendData:data];
}];
Probably you want to repeat the same above for task.standardError.
IMPORTANT:
When your task terminates, you have to set readabilityHandler block to nil; otherwise, you'll encounter high CPU usage, as the reading will never stop.
[task setTerminationHandler:^(NSTask *task) {
// do your stuff on completion
[task.standardOutput fileHandleForReading].readabilityHandler = nil;
[task.standardError fileHandleForReading].readabilityHandler = nil;
}];
This is all asynchronous (and you should do it async), so your method should have a ^completion block.