Here's my implementation based on the above answers, to be called from applicationDidFinishLaunching:
#import "NSTask+OneLineTasksWithOutput.h"
void FixUnixPath() {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void){
NSString *userShell = [[[NSProcessInfo processInfo] environment] objectForKey:@"SHELL"];
NSLog(@"User's shell is %@", userShell);
BOOL isValidShell = NO;
for (NSString *validShell in [[NSString stringWithContentsOfFile:@"/etc/shells" encoding:NSUTF8StringEncoding error:nil] componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]) {
if ([[validShell stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] isEqualToString:userShell]) {
isValidShell = YES;
break;
}
}
if (!isValidShell) {
NSLog(@"Shell %@ is not in /etc/shells, won't continue.", userShell);
return;
}
NSString *userPath = [[NSTask stringByLaunchingPath:userShell withArguments:[NSArray arrayWithObjects:@"-c", @"echo $PATH", nil] error:nil] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (userPath.length > 0 && [userPath rangeOfString:@":"].length > 0 && [userPath rangeOfString:@"/usr/bin"].length > 0) {
NSLog(@"User's PATH as reported by %@ is %@", userShell, userPath);
setenv("PATH", [userPath fileSystemRepresentation], 1);
}
});
}
P.S. The reason this works is because it catches environment changes made by the shell. E.g. RVM adds PATH=$PATH:$HOME/.rvm/bin
to .bashrc on installation. Cocoa apps are launched from launchd, so they don't have these changes in their PATH.
I'm not 100% satisfied with this code, because it does not catch everything. My original intent was to handle RVM specifically, so I had to use a non-login shell here, but in practice, people randomly put PATH modification into .bashrc and .bash_profile, so it would be best to run both.
One of my users even had an interactive menu (!!!) in his shell profile, which naturally lead to this code hanging and me exporting a shell env flag just for him. :-) Adding a timeout is probably a good idea.
This also assumes that the shell is bourne-compatible and thus does not work with fish 2.0, which is getting increasingly more popular among the hacker community. (Fish considers $PATH an array, not a colon-delimited string. And it thus prints it using spaces as delimiters by default. One can probably cook up an easy fix, like running for i in $PATH; echo "PATH=$i"; end
and then only taking the lines that start with PATH=
. Filtering is a good idea on any case, because profile scripts often print something on their own.)
As a final note, this code has been an important part of a shipping app for over a year (top 10 paid developer tool on the Mac App Store for most of the year). However, I'm now implementing sandboxing and taking it out; naturally, you cannot do this trick from a sandboxed app. I'm replacing it with explicit support for RVM and friends, and reproducing their respective env changes manually.
For those wishing to use something like system Git from a sandboxed app, note that while you don't have access to read files and enumerate directories, you do have access to stat — [[NSFileManager defaultManager] fileExistsAtPath:path]
. You can use this to probe a hard-coded list of typical folders looking for your binary, and when you find the locations (like /usr/local or /opt/local or whatever), ask the user to give you access via NSOpenPanel. This won't catch every case, but will handle 90% of use cases and is the best thing you can do for your users out of the box.