NSTask Question - objective-c

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?

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 works in xcode run but not when i run it outside xcode

I have created this app that make use of the /usr/bin/say command in OS X.
This method takes the value from a textfield and uses "say" to save it.
But when i run this outside xcode. I don't get the saved file.
- (IBAction)save:(id)sender
{
NSString *path;
path = #"/usr/bin/say";
NSArray *args;
args = [NSArray arrayWithObjects: #"-o", #"text.wav", #"--data-format=LEF32#8000", [textField stringValue], nil];
[[NSTask launchedTaskWithLaunchPath:path arguments:args] waitUntilExit];
NSLog(#"Saved");
}
Anyone know what i'm doing wrong?
Perhaps it's being saved but you don't know where. The path text.wav is a partial path, so it's going to be saved in the "current directory". But what directory is the "current directory"? You probably don't know.

Run user defined command with NSTask

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.

How to insert some data into a string of AppleScript

I have a function that sends a string "theData". I want to insert that string here in this code. Would someone please tell me the correct syntax for this? Things get a bit hairy with the \"s and the "s. Thanks!
NSTask *task;
task = [[NSTask alloc] init];
[task setLaunchPath:#"/usr/bin/osascript"];
[task setArguments:[NSArray arrayWithObjects:#"-e", #"tell application \"System Events\"\n",
#"-e", #" keystroke \"" + theData + "\"",
#"-e", #"end tell", nil]];
[task launch];
icktoofay already gave the more correct answer, but let me just show how to insert a string in a string:
NSString* toBeInserted = #"for";
NSString* result = [NSString stringWithFormat:#"in%#mation",toBeInserted];
NSLog(#"%#",result);
This gives information. For more details, read Apple's doc.
I mean, Apple's doc is quite good, actually. Read it before asking a question here at SO.
By the way, you don't have to launch osascript to execute AppleScript. You can use NSAppleScript as in
NSAppleScript* script=[[NSAppleScript alloc] initWithSource:#"tell app \"Finder\" to activate "];
NSDictionary*error;
[script executeAndReturnError:&error];
[script release];
Well, NSAppleScript is an oddity which requires NSDictionary, not an NSError, to report an error ...
Or, you can use Scripting Bridge to map AppleScript objects to Objective-C objects.
I see you have another way of doing it, but you can use format strings to accomplish this
[NSString stringWIthFormat: #"part one %# part 2", theData];
Assuming the data is an nsstring containing "hello", this will give you:
#"part one hello part 2"