0

I am using Xcode 6.4 trying to execute a find command. The application is a wrapper around the find command. We have a SAN that finder will not search however the find command will search the SAN, I don't wish to discuss the issues with the SAN.

I wrote the application originally in Swift in a few short minutes. Unfortunately it will not deploy to OSX 10.8 so I proceeded to rewrite in Objective C.

I suspect the problem I'm having is related to the way I am constructing the arguments array. The only examples I came across after Googling for 3 days all had hard coded literals for the arguments, in the real world we use variables.

Here is the basic code associated when I click the "Search" button.

NSString* newShell = @"-c";
NSString* commandFind = @"find";
NSString* optionName = @"-iname";
NSString* searchFor = @"\"";
NSString* searchPath = @"\"";
searchPath = [searchPath stringByAppendingString:_searchPathOutlet.stringValue];
searchPath = [searchPath stringByAppendingString:@"\""];
NSString* searchWildCard = @"*";
NSString* searchWord = _searchWordsOutlet.stringValue;
searchFor = [searchFor stringByAppendingString:searchWildCard];
searchFor = [searchFor stringByAppendingString:searchWord];
searchFor = [searchFor stringByAppendingString:searchWildCard];
searchFor = [searchFor stringByAppendingString:@"\""];
NSLog(@"%@",searchPath); //debug
NSLog(@"%@",searchFor); //debug
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:@"/bin/sh"];
NSArray *arguments = [NSArray arrayWithObjects: newShell, commandFind, searchPath, optionName, searchFor, nil];
NSString *stringRep = [NSString stringWithFormat:@"%@",arguments]; //debug
NSLog(@"%@",stringRep); //debug
[task setArguments:arguments];
NSPipe* pipe = [NSPipe pipe];
[task setStandardOutput:pipe];
[task launch];
[task waitUntilExit]; // Alternatively, make it asynchronous.
NSData *outputData = [[pipe fileHandleForReading] readDataToEndOfFile];
NSString *outputString = [[NSString alloc] initWithData:outputData encoding:NSUTF8StringEncoding];
_searchResultsOutlet.string = outputString;



The output is as follows:
2015-07-16 12:09:39.141 APSAETVSANSearch[2716:68456] "/Users/test/Downloads"
2015-07-16 12:09:39.141 APSAETVSANSearch[2716:68456] "*adobe*"
2015-07-16 12:09:39.141 APSAETVSANSearch[2716:68456] (
"-c",
find,
"\"/Users/test/Downloads\"",
"-iname",
"\"*adobe*\""
)
usage: find [-H | -L | -P] [-EXdsx] [-f path] path ... [expression]
find [-H | -L | -P] [-EXdsx] -f path [path ...] [expression]

I agree with both you and trojanfoe. I am new to objective-c and I find the syntax a little strange compared to other languages but I have some working code listed below. I would like to run the function doSearch as a callback in an asynchronous thread. I am familiar with Microsoft's threading related to passing "thread safe" parameters using delegates to the thread's callback function (e.g. searchPathURL, searchWords, textView) but can't seem to find an example that is appropriate via google. Here is my function can you get me started?

- (IBAction)buttonSearch_Click:(id)sender {
doSearch(directoryURL, _searchWordOutlet.stringValue, _textViewOutlet);
}

void doSearch(NSURL *searchPathURL, NSString *searchWords, NSTextView *textView){
NSArray *keys = [NSArray arrayWithObjects:
NSURLIsDirectoryKey, NSURLIsPackageKey, NSURLLocalizedNameKey, nil];
NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager]
enumeratorAtURL:directoryURL
includingPropertiesForKeys:keys
options:(NSDirectoryEnumerationSkipsHiddenFiles)
errorHandler:^(NSURL *url, NSError *error) {
 // Handle the error.
 // Return YES if the enumeration should continue after the error.
 return YES;
}
];
for (NSURL *url in enumerator) {
// Error-checking is omitted for clarity.

NSNumber *isDirectory = nil;
[url getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:NULL];

if ([isDirectory boolValue]) {
NSString *localizedName = nil;
[url getResourceValue:&localizedName forKey:NSURLLocalizedNameKey error:NULL];

NSNumber *isPackage = nil;
[url getResourceValue:&isPackage forKey:NSURLIsPackageKey error:NULL];

if ([isPackage boolValue]) {
//Its a package
//NSLog(@"Package at %@", localizedName);
}
else {
//Its a directory
//NSLog(@"Directory at %@", localizedName);
}
}
else {
//Its a file
NSString *searchPath = [url.path lowercaseString];
NSString *searchText = [searchWords lowercaseString];
if ([searchPath containsString:searchText]) {
NSLog(@"%@", url.path);
[textView insertText:url.path];
[textView insertText:@"\n"];
}
}
}
}
Jim Dandy BOA
  • 533
  • 7
  • 13
  • Why don't you implement the functionality you want in code instead? It'll be much quicker and much more efficient. – trojanfoe Jul 16 '15 at 16:38
  • I have many users that will be using this tool, and none of them know how to use a Terminal session (assuming that's what you mean by "in code instead"). I really need a GUI that anyone can use without any coding knowledge. Do you have any suggestions on how to fix the code? I know this has to be something very simple as I am new to objective-c. – Jim Dandy BOA Jul 16 '15 at 17:34
  • No I meant writing code to do what `find` does; which is basically look in directories for files match certain criteria. Forking a shell process for tasks that can be done in the program is normally a bad idea if the program cannot do the task itself. – trojanfoe Jul 16 '15 at 17:43
  • Great suggestion! I guess I could try using NSDirectoryEnumerator but It's possible that it will have the same problem searching the SAN directories that Finder has assuming that Finder uses the enumerator as well. After I get the initial shell tool properly wrapped the staff can use that right away and then I will try the API. The company that made the SAN went out of business 2 years ago and we can't update the OS on the metadata controllers. – Jim Dandy BOA Jul 16 '15 at 18:57

1 Answers1

2

When you use /bin/sh -c <command> the command has to be one argument. That is, at a shell, you can't do:

/bin/sh -c find "/Users/test/Downloads" -iname "*adobe*"

You have to do:

/bin/sh -c 'find "/Users/test/Downloads" -iname "*adobe*"'

So, just make one single string whose content is equivalent to:

NSString* command = @"find \"/Users/test/Downloads\" -iname \"*adobe*\"";

and use an argument array equivalent to @[ @"-c", command ].

Alternatively, if you don't need the shell to process the string (and, in your example, you don't), you should just set the task's launch path to @"/usr/bin/find" and set the arguments to @[ @"/Users/test/Downloads", @"-iname", @"*adobe*" ]. Using a shell when you don't need it only adds danger and inefficiency. For example, if your user enters "$(rm -rf ~)" in your text field, they will be very unhappy when you run the task. Less destructive but more likely is if the directory path or search term contains a double quote (") character.

All of that said, I concur with trojanfoe that you should do this programmatically rather than launching a subprocess. If NSDirectoryEnumerator doesn't work for some reason, you can use POSIX/BSD APIs.


Update in response to your updated question:

To run a task in the background, you can use Grand Central Dispatch (GCD). For example, your -buttonSearch_Click: method could be written like this:

- (IBAction)buttonSearch_Click:(id)sender {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        doSearch(directoryURL, _searchWordOutlet.stringValue, _textViewOutlet);
    });
}

However, you can't update the GUI from a background thread. So, your doSearch() function needs to shunt any manipulation of the text view back to the main thread. It can do this using code like the following:

dispatch_async(dispatch_get_main_queue(), ^{
    [textView insertText:url.path];
    [textView insertText:@"\n"];
});

By the way, your check if the search term is in the path is not the same as what the find command you started with does. You're checking the whole path, including parent directories, while the find command only checked each item's file name. You can get the file name from an NSURL by requesting its lastPathComponent rather than its path.

Also, to do a case-insensitive check if one string contains another, you should not lowercase boths strings and then call -containsString:. You should just use -localizedCaseInsensitiveContainsString: without lowercasing manually. (Or, if you don't want locale-appropriate case-insensitivity, you can use -rangeOfString:options: with NSCaseInsensitiveSearch for the options.)

Ken Thomases
  • 88,520
  • 7
  • 116
  • 154
  • I agree with both you and trojanfoe. I am new to objective-c and I find the syntax a little strange compared to other languages but I have some working code listed below. I would like to runthe function doSearch as a callback in an asynchronous thread. I am familiar with Microsoft's threading related to passing "thread safe" parameters to the thread's callback function (e.g. searchPathURL, searchWords, textView) but can't seem to find an example that is appropriate via google. Here is my function can you get me started? – Jim Dandy BOA Jul 17 '15 at 12:30
  • This question about doing the search on a background thread should really have been a separate question, but I updated my answer to address it. – Ken Thomases Jul 18 '15 at 04:10