Run user defined command with NSTask - objective-c

I would like to execute a terminal command specified by the user. For example, the user might write killall "TextEdit" or say "Hello world!" in a text field, and I want to execute that command.
NSTask is the way to go, except I have two problems with it:
First: the arguments. Right now I'm doing this:
NSArray* args = [commandString componentsSeparatedByString: #" "];
[task setArguments: [args subarrayWithRange: NSMakeRange(1, [args count] - 1)]]; // First one is the command name
Is this the way to do it? I don't think I've had problems with this yet, but I doesn't look like it's safe. Imagine this: the user writes killall 'Address Book' but the command receives as arguments 'Address and Book'?? That doesn't work. So, what should I do instead? How can I safely parse the arguments?
Second: the launch path. It's much more user-friendly to only have to write the name of the command, instead of the complete path to it. So I want to support that, which means finding out programmatically the full path for a command having only it's name. For that I wrote a category on NSTask like this:
+ (NSString*)completePathForExec: (NSString*)exec
{
NSTask* task = [[NSTask alloc] init];
NSPipe* pipe = [[NSPipe alloc] init];
NSArray* args = [NSArray arrayWithObject: exec];
[task setLaunchPath: #"/usr/bin/which"];
[task setArguments: args];
[task setStandardOutput: pipe];
[task setStandardError: pipe];
[task launch];
[task waitUntilExit];
NSFileHandle* file = [pipe fileHandleForReading];
NSString* result = [[NSString alloc] initWithData: [file readDataToEndOfFile] encoding: NSASCIIStringEncoding];
if ([result length]) {
if ([result hasSuffix: #"\n"]) { result = [result substringWithRange: NSMakeRange(0, [result length] - 1)]; }
return result;
}
else { return exec; }
}
This seems to works well. However, how can I be sure that this path: /usr/bin/which will always work? I mean: will it work on 10.6, 10.7, 10.8, etc? I think I had a problem once where the path to a shell command changed with the system version, and you can never be too careful.
If the path is guaranteed to stay the same, then this isn't a problem. If it changes, then how can I know the 'path to the path-finder'?

It'll be far easier for you to not re-invent the command line parsing wheel. But, of course, going down the route of executing arbitrary user entered code is a security nightmare (tempered by the fact that the user has access to the system and, thus, could probably just run Terminal directly).
Specifically, have NSTask wrap an invocation of one of the shells with the command line option to have it execute an arbitrary string.
sh -c "ls -alF"
This would allow you to pass the path to sh as your launch path, which is in a fixed location on every system. The #"-c" argument tells sh to parse the next argument as a script and, of course, the next argument is whatever the user entered.
Note, this will also give the user the ability to pipe stuff, too.

Related

How do I force an command line argument string to be quoted

I have a program which should cause 2 files to be compared using FileMerge.
This does work, but occasionally fails. I suspect this is when the paths passed as arguments contain space characters.
The following code fragment constructs the task and launches it.
NSTask *task = [[NSTask alloc] init];
NSPipe *pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
[task setStandardInput:[NSPipe pipe]]; //The magic line that keeps your log where it belongs
NSFileHandle *file = [pipe fileHandleForReading];
[task setLaunchPath: #"/bin/sh"];
NSArray *arguments = [NSArray arrayWithObjects:
#"-c" ,
[[NSUserDefaults standardUserDefaults] stringForKey:PREF_COMPARE_COMMAND],
#"Compare", // $0 place holder
source,
target,
nil];
[task setArguments:arguments];
[task setEnvironment:[NSDictionary dictionaryWithObject:#"/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin" forKey:#"PATH"]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(pipeReadCompletionNotification:)
name:NSFileHandleReadCompletionNotification
object:file];
[file readInBackgroundAndNotify];
[task launch];
I have tried many options to try and escape spaces or enclose the paths in quotes, without success. I would welcome any suggestions.
A typical arguments as a result of a run is:-
"-c",
"opendiff $1 $2",
Compare,
"/Users/ian/temp/Indian Pacific/RailRes Travel Documentation1.pdf",
"/Users/ian/temp/Indian Pacific/RailRes Travel Documentation.pdf"
I have tried
[source stringByReplacingOccurrencesOfString:#" " withString:#"\\ "],
[source stringByReplacingOccurrencesOfString:#" " withString:#"\ "],
The 1st actually inserts \\ the second produces a compile error unknown escape sequence
I tried Ken Thomases's suggestion (knowing my names don't have')
[[#"'" stringByAppendingString:source] stringByAppendingString:#"'"],
[[#"'" stringByAppendingString:target] stringByAppendingString:#"'"],
Unfortunately this resulted in arguments
"-c",
"opendiff $1 $2",
Compare,
"'/Users/ian/temp/Indian Pacific/RailRes Travel Documentation1.pdf'",
"'/Users/ian/temp/Indian Pacific/RailRes Travel Documentation.pdf'"
and failed in the same way. /Users/ian/temp/Indian does not exist
Edit _______________________ Working Code _____________________________________
NSArray *arguments = [NSArray arrayWithObjects:
#"-c" ,
[NSString stringWithFormat:#"%# '%#' '%#'", #"opendiff", source, target],
nil];
The -c option to the shell takes a single string as argument, not multiple arguments. Create the complete shell command line as an NSString using stringWithFormat. In that string you should escape the file names just as you would in the terminal, e.g. surround them with single quotes. Pass this string as the argument after #"-c".
HTH
There are a number of special characters that the shell interprets. Using double quotes is not sufficient to make a string safe against that. You could try escaping all of the special characters, but that can be finicky.
The easiest, safest approach is to use single quotes. Those tell the shell to treat everything up to the next single quote verbatim with no interpretation. The only thing you need to take care about is if your string contains single quotes themselves. So, the following two lines will sanitize your source argument:
NSString* quoted_source = [source stringByReplacingOccurrencesOfString:#"'" withString:#"'\\''"];
quoted_source = [[#"'" stringByAppendingString:quoted_source] stringByAppendingString:#"'"];
The first line turns any embedded single quote into an end single quote (we'll get to the beginning one in a moment), an escaped single quote to take the place of the one we're replacing, followed by a new open single quote. The second line wraps the whole thing with an opening single quote at the beginning and an ending one at the end.

NSTask with if statement?

I'm not really sure how to phrase this question, but I'll try my best. I have an NSTask that currently unzips an archive, and it needs to also support archives which might have a password. I have no idea how to go about pausing the task or how to detect if a password might be present, get the input if needed then proceed, or if this is even possible?
NSTask *unzip = [[NSTask alloc] init];
[unzip setLaunchPath:#"/usr/bin/unzip"];
[unzip setArguments:[NSArray arrayWithObjects:#"-u", #"-d",
destination, zipFile, nil]];
NSPipe *aPipe = [[NSPipe alloc] init];
[unzip setStandardOutput:aPipe];
[unzip launch];
// if statement here?
[unzip waitUntilExit];
[unzip release];
From terminal stdout looks like this:
Archive: encrypted.zip
creating: test
[encrypted.zip] test password:
skipping: test incorrect password
Also I wish not to limit this to zip files, since I also use a similar task to untar/gzip archives.
Is there are way to have the NSTask pop up a modal window if it detects an archive with a password or from certain stdout it might receive?

NSTask Question

I'm trying to get my NSTask to unzip a file for me. This works fine if the path has no spaces, but when it does, it can't find any of the files. I can't hardcode the " signs because I'm storing the zip file in a temporary folder, which is assigned by the system.
Does anyone know how to achieve this?
Here's my code:
NSTask*task = [[NSTask alloc] init];
[task setLaunchPath:#"/usr/bin/unzip"];
NSArray*arguments = [NSArray arrayWithObjects:zipPath,#"-d",path,nil];
[task setArguments:arguments];
[task launch];
[task release];
Why can't you embed the quote marks?
NSString *quotedPath = [NSString stringWithFormat:#"\"%#\"", path];
NSArray *arguments = [NSArray arrayWithObjects:zipPath, #"-d", quotedPath, nil];
Having a space in the argument does not look like your problem - note that the console is showing the pathname with a space. An argument with a space is passed as a single argument, I've just confirmed it will happily unzip #"a space.zip". Have you checked the file does exist where you think it does and you have access to it?
Could you parse the path components using NSString's - (NSArray *)pathComponents method, add the quotes where needed, then rebuild the string using (NSString *)pathWithComponents:(NSArray *)components
Does that work?

Is there an Instruments API?

Is it possible to setup an Instruments run programmatically from my code? For instance, I'd like to structure my code something like this where startTrace might setup a specific probe for the current thread and start recording while stopTrace would stop recording. I would be writing the content of those routines using the Instruments API that is the subject of this question.
-(void)myInterestingMethod
{
[self startTrace];
// do something interesting and performance critical
[self stopTrace];
}
If the above isn't available, is setting up my own DTrace probe a viable alternative?
Doesn't look like there's anything straight-forward, but there is an instruments command-line tool. Here's some quick+dirty code that will invoke it and sample CPU usage for the calling process
static void sampleMe() {
// instruments -t '/Developer/Applications/Instruments.app/Contents/Resources/templates/CPU Sampler.tracetemplate' -p 26838 -l 5000
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:#"/usr/bin/instruments"];
[task setArguments:[NSArray arrayWithObjects:
#"-t",
#"/Developer/Applications/Instruments.app/Contents/Resources/templates/CPU Sampler.tracetemplate",
#"-p",
[NSString stringWithFormat:#"%ld", getpid()],
#"-l",
#"5000",
nil]];
[task setCurrentDirectoryPath:NSHomeDirectory()];
[task setStandardInput:[NSPipe pipe]];
[task setStandardOutput:[NSPipe pipe]];
[task setStandardError:[NSPipe pipe]];
[task launch];
// purposely leak everything since I can't be bothered to figure out lifetimes
}
After invocation a file named instrumentscli0.trace will be in your home directory.
Update: Instruments 4.0 offers DTSendSignalFlag in the DTPerformanceSession for iOS apps.

Registering for a NSFileHandleReadToEndOfFileCompletionNotification

I'm trying to figure out how to make this piece of code from a previous question work, but I'm stuck on the part on how to 'register' a NSFileHandleReadToEndOfFileCompletionNotification.
This is my code:
NSTask *topTask = [NSTask new];
[topTask setLaunchPath:#"/usr/bin/top"];
[topTask setArguments:[NSArray arrayWithObjects:#"-s", #"1", #"-l", #"3600", #"-stats", #"pid,cpu,time,command", nil]];
NSPipe *outputPipe = [NSPipe pipe];
[topTask setStandardOutput:outputPipe];
[topTask launch];
... which runs fine until I add this:
[[outputPipe fileHandleForReading] readToEndOfFileInBackgroundAndNotify];
... which causes the program to freeze. And when I add this:
NSString *outputString = [[[NSString alloc] initWithData:[[notification userInfo] objectForKey:NSFileHandleNotificationDataItem] encoding:NSUTF8StringEncoding] autorelease];
... the code doesn't compile and I get the !warning
'notification' undeclared.
Any assistance on this matter earns copious amounts of gratitude on my behalf.
Running top with those parameters in the command line will cause it to continually print out stats and it will never write EOF. That's why -readToEndOfFileInBackgroundAndNotify runs forever.
It sounds like you may just want to read the first iteration and then kill the task.
As for the error, it sounds like you simply don't have a variable named notification in the method containing that line.