Run NSTask from .command - objective-c

Sorry, english is not my first language, and I have a poor grammar skill.
Hi, I am a developer that is very new to Objective-C, and I have some problems with using NSTask. I have a .command file that I want to execute in this cocoa application, but if I use the "open" command using NSTask to execute the file, it launches terminal. Is there a way to just execute it without launching it, like a typical NSTask? Or can I just have a text file with the command to be executed? Thank You.
Here is my code...
NSString *pathtorb = [[NSBundle mainBundle] pathForResource:#"rightSpace" ofType:#"command"];
NSTask *DfileBlankSpace = [[NSTask alloc]init];
DfileBlankSpace.launchPath = #"/usr/bin/open";
DfileBlankSpace.arguments = #[pathtorb];
[DfileBlankSpace launch];
[DfileBlankSpace waitUntilExit];
NSTask *killDock = [[NSTask alloc]init];
killDock.launchPath = #"/usr/bin/killall";
killDock.arguments = #[#"Dock"];
[killDock launch];
[killDock waitUntilExit];

A .command is just a shell script, instead of using open - which associates these files with Terminal - run sh passing it -c and the file pathname as arguments:
DfileBlankSpace.launchPath = #"/bin/sh";
DfileBlankSpace.arguments = #[#"-c", pathtorb];
HTH
Addendum
As #KenThomases as pointed out in the comments, if you have not escaped pathorb to make it a valid bash pathname then you should omit the #"-c" argument. This has the shell read from the file without parsing its file name as though typed on the command line.

Related

How to run a shell script?

I have the following shell script:
#!/bin/sh
cd $1
$1 is the path that I want to pass from the main.m which will call the script.
The script is located in the same directory as main.m.
How can I call a local script within the project?
Is it possible to compile the script into a binary so that when I execute the binary, it will also call the script?
Thanks
I assume you are trying to run an embedded script file in macOS app or command line tool mode, you need wrap the script file with NSTask to implement it.
NSTask *task = [[NSTask alloc] init];
task.launchPath = #"/bin/bash";
task.arguments = #[#"/path/to/your_script_file"];
[task launch];
// Specify the stdout and stderr to pipe if necessary.
NSPipe *pipe = [NSPipe pipe];
[task setStandardOutput:pipe];
[task setStandardError:pipe];
Note that NSTask is available in macOS only.

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?

currentDirectoryPath and NSTask

OK, let's say I'm creating a (Bash) Terminal emulator - I'm not actually, but it's pretty close in terms of description.
I've managed to get (almost) everything working, however I'm facing one simple issue: maintaining the current directory.
I mean... let's say the user runs pwd and we execute this via NSTask and /usr/bin/env bash. This outputs the current app's directory. That's fine.
Now, let's say the user enters cd ... The path is changing right? (OK, even for that particular session, but it is changing, nope?)
So, I though of storing the task's currentDirectoryPath when the Task is terminated and then re-set it when starting another Bash-related task.
However, it keeps getting the very same path (the one the app bundle is in).
What am I missing?
Any ideas on how to get this working?
The code
- (NSString*)exec:(NSArray *)args environment:(NSDictionary*)env action:(void (^)(NSString*))action completed:(void (^)(NSString*))completed
{
_task = [NSTask new];
_output = [NSPipe new];
_error = [NSPipe new];
_input = [NSPipe new];
NSFileHandle* outputF = [_output fileHandleForReading];
NSFileHandle* errorF = [_error fileHandleForReading];
__block NSString* fullOutput = #"";
NSMutableDictionary* envs = [NSMutableDictionary dictionary];
envs[#"PATH"] = [[APP environment] envPath];
[_task setLaunchPath:#"/usr/bin/env"];
if (env)
{
for (NSString* key in env)
{
envs[key] = env[key];
}
}
[_task setEnvironment:envs];
[_task setArguments:args];
[_task setStandardOutput:_output];
[_task setStandardError:_error];
[_task setStandardInput:_input];
void (^outputter)(NSFileHandle*) = ^(NSFileHandle *file){
NSData *data = [file availableData];
NSString* str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
action(str);
fullOutput = [fullOutput stringByAppendingString:str];
};
[outputF setReadabilityHandler:outputter];
[errorF setReadabilityHandler:outputter];
[_task setTerminationHandler:^(NSTask* task){
completed(fullOutput);
dispatch_async(dispatch_get_main_queue(), ^{
[[APP environment] setPwd:[task currentDirectoryPath]];
});
[task.standardOutput fileHandleForReading].readabilityHandler = nil;
[task.standardError fileHandleForReading].readabilityHandler = nil;
[task.standardInput fileHandleForWriting].writeabilityHandler = nil;
[task terminate];
task = nil;
}];
if (![[[APP environment] pwd] isEqualToString:#""])
[_task setCurrentDirectoryPath:[[APP environment] pwd]];
[_task launch];
return #"";
}
As a general matter, it is difficult to impossible to modify another process's environment and other properties from the outside. Similarly, it is not generally possible to query those from the outside. Debuggers and ps can do it by using special privileges.
The parent process which created the process in question has the opportunity to set the initial environment and properties at the point where it spawns the subprocess.
The cd command is necessarily a shell built-in command precisely because it has to modify the shell process's state. That change does not directly affect any other existing process's state. It will be inherited by subprocesses that are subsequently created.
The currentDirectoryPath property of NSTask is only meaningful at the point where the task is launched. It is the current directory that the new process will inherit. It does not track the subprocess's current directory, because it can't. Querying it only returns the value that the NSTask object was configured to use (or the default value which is the current directory of the process which created the NSTask object).
If you're trying to write something like a terminal emulator, you will need to create a long-running interactive shell subprocess with communication pipes between the parent and the shell. Don't run individual commands in separate processes. Instead, write the commands to the interactive shell over the pipe and read the subsequent output. It probably doesn't make sense to try to interpret that output since it can be general in form and not easily parsable. Just display it directly to the user.
Alternatively, you will have to interpret some commands locally in the parent process. This will be analogous to shell built-ins. So, you would have to recognize a cd command and, instead of launching an NSTask to execute it, you would just modify the state of the parent process in such a way that the new current directory will be used for subsequent tasks. That is, you could track the new current directory in a variable and set currentDirectoryPath for all subsequent NSTask objects before launching them or you could modify the parent process's current directory using -[NSFileManager changeCurrentDirectoryPath:] and that will automatically be inherited by future subprocesses.
I am fairly certain that running cd in NSTask does not change the value of task.currentDirectoryPath. Have you tried setting a break point in your dispatch call to see if that value is actually being set correctly?
Edit:
From your termination handler try doing [[APP environment] setPwd:[[[NSProcessInfo processInfo]environment]objectForKey:#"PATH"]];

Cocoa code for launching an app with parameters

I want a Cocoa equivalent of the command line tool open(1), especially for the usage in this form:
open -a <SomeApplication> <SomeFile> --args <Arg1> <Arg2> ...
Take QuickTime as an example. The following command line will open a file using QuickTime, and the arguments can control if QuickTime plays the file on startup.
open -a "QuickTime Player" a.mp4 --args -MGPlayMovieOnOpen 0 # or 1
I have read Launching an Mac App with Objective-C/Cocoa. prosseek's answer, which I think is equivalent to open -a <App> <File>, works well when I do not specify any argument. ughoavgfhw's answer, which I think is equivalent to open -a <App> --args <Arg1> <Arg2> ..., works well when I do not specify any file to open. But neither can specify a filename and arguments at the same time.
I have also tried to append the filename to the argument list, which is the common way used by unix programs. It seems that some applications can accept it, but some cannot. QuickTime prompts an error saying it cannot open the file. I am using the following code.
NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
NSURL *url = [NSURL fileURLWithPath:[workspace fullPathForApplication:#"QuickTime Player"]];
NSArray *arguments = [NSArray arrayWithObjects:#"-MGPlayMovieOnOpen", #"0", #"a.mp4", nil];
[workspace launchApplicationAtURL:url options:0 configuration:[NSDictionary dictionaryWithObject:arguments forKey:NSWorkspaceLaunchConfigurationArguments] error:nil];
// open -a "QuickTime Player" --args -MGPlayMovieOnOpen 0 a.mp4
It seems that the mechanism in opening files differs from usual arguments. Can anyone explain the internals of open(1), or just give me a solution? Thanks.
You might want to pipe the output of the task so you know the results. "a.mp4" needs to be the full path to the file.
NSArray *args = [NSArray arrayWithObjects:#"-a", #"QuickTime Player", #"--args", #"a.mp4", nil];
NSTask *task = [NSTask new];
[task setLaunchPath:#"/usr/bin/open"];
[task setArguments:args];
[task launch];

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.