简体   繁体   中英

Xcode Cocoa Objective C Using NSTask to Execute “Find” Shell Command Returns Syntax

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 (eg 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"];
}
}
}
}

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.)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM