Finding the number of PDFs in a folder [duplicate] - macos

This question already has an answer here:
Use NSOpenpanel and NSfilemanager to find contents of directory
(1 answer)
Closed 10 years ago.
I have a button in an app that when clicked should cause a dialog box to open. The user then chooses a folder, clicks OK, and then the app displays the number of PDF files in that folder.
I have the following code implemented below. How do I scan a folder for the amount of PDF files in it?
- (IBAction)selectPathButton:(NSButton *)sender {
// Loop counter.
int i;
// Create a File Open Dialog class.
NSOpenPanel* openDlg = [NSOpenPanel openPanel];
// Set array of file types
NSArray *fileTypesArray;
fileTypesArray = [NSArray arrayWithObjects:#"pdf", nil];
// Enable options in the dialog.
[openDlg setCanChooseFiles:YES];
[openDlg setAllowedFileTypes:fileTypesArray];
[openDlg setAllowsMultipleSelection:TRUE];
// Display the dialog box. If the OK pressed,
// process the files.
if ( [openDlg runModal] == NSOKButton ) {
// Gets list of all files selected
NSArray *files = [openDlg URLs];
// Loop through the files and process them.
for( i = 0; i < [files count]; i++ ) {
}
NSInteger payCount = [files count];
self.payStubCountLabel.stringValue = [NSString stringWithFormat:#"%ld", (long)payCount];
}
}

get files and directories under path
[NSFileManager]- (NSArray *)contentsOfDirectoryAtPath:(NSString *)path error:(NSError **)error;
get files and directories under path and subpath
[NSFileManager]- (NSArray *)subpathsOfDirectoryAtPath:(NSString *)path error:(NSError **)error;
then filter out pdf files.
NSMutableArray *pdfFiles = [NSMutableArray array];
for(NSString *fileName in files) {
NSString *fileExt = [fileName pathExtension];
if([fileExt compare:#"pdf" options:NSCaseInsensitiveSearch] == NSOrderSame) {
[pdfFiles addObject:fileName];
}
}
now what you want is in pdfFiles.
NSUInteger pdfFilesCount = [pdfFiles count];
if you only want count of pdf files, just using a variable with forin loop.

Related

How to read all remaining output of readInBackgroundAndNotify after NSTask has ended?

I'm invoking various command line tools via NSTask. The tools may run for several seconds, and output text constantly to stdout. Eventually, the tool will terminate on its own. My app reads its output asynchronously with readInBackgroundAndNotify.
If I stop processing the async output as soon as the tool has exited, I will often lose some of its output that hasn't been delivered by then.
Which means I have to wait a little longer, allowing the RunLoop to process pending read notifications. How do I tell when I've read everything the tool has written to the pipe?
This problem can be verified in the code below by removing the line with the runMode: call - then the program will print that zero lines were processed. So it appears that at the time the process has exited, there's already a notification in the queue that is waiting to be delivered, and that delivery happens thru the runMode: call.
Now, it might appear that simply calling runMode: once after the tool's exit may be enough, but my testing shows that it isn't - sometimes (with larger amounts of output data), this will still only process parts of the remaining data.
Note: A work-around such as making the invoked tool outout some end-of-text marker is not a solution I seek. I believe there must be some proper way to do this, whereby the end of the pipe stream is signalled somehow, and that's what I'm looking for in an answer.
Sample Code
The code below can be pasted into a new Xcode project's AppDelegate.m file.
When run, it invokes a tool that generates some longer output and then waits for the termination of the tool with waitUntilExit. If it would then immediately remove the outputFileHandleReadCompletionObserver, most of the tool's output would be missed. By adding the runMode: invocation for the duration of a second, all output from the tool is received - Of course, this timed loop is less than optimal.
And I would like to keep the runModal function synchronous, i.e. it shall not return before it has received all output from the tool. It does run in its own tread in my actual program, if that matters (I saw a comment from Peter Hosey warning that waitUntilExit would block the UI, but that would not be an issue in my case).
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[self runTool];
}
- (void)runTool
{
// Retrieve 200 lines of text by invoking `head -n 200 /usr/share/dict/words`
NSTask *theTask = [[NSTask alloc] init];
theTask.qualityOfService = NSQualityOfServiceUserInitiated;
theTask.launchPath = #"/usr/bin/head";
theTask.arguments = #[#"-n", #"200", #"/usr/share/dict/words"];
__block int lineCount = 0;
NSPipe *outputPipe = [NSPipe pipe];
theTask.standardOutput = outputPipe;
NSFileHandle *outputFileHandle = outputPipe.fileHandleForReading;
NSString __block *prevPartialLine = #"";
id <NSObject> outputFileHandleReadCompletionObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSFileHandleReadCompletionNotification object:outputFileHandle queue:nil usingBlock:^(NSNotification * _Nonnull note)
{
// Read the output from the cmdline tool
NSData *data = [note.userInfo objectForKey:NSFileHandleNotificationDataItem];
if (data.length > 0) {
// go over each line
NSString *output = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSArray *lines = [[prevPartialLine stringByAppendingString:output] componentsSeparatedByString:#"\n"];
prevPartialLine = [lines lastObject];
NSInteger lastIdx = lines.count - 1;
[lines enumerateObjectsUsingBlock:^(NSString *line, NSUInteger idx, BOOL * _Nonnull stop) {
if (idx == lastIdx) return; // skip the last (= incomplete) line as it's not terminated by a LF
// now we can process `line`
lineCount += 1;
}];
}
[note.object readInBackgroundAndNotify];
}];
NSParameterAssert(outputFileHandle);
[outputFileHandle readInBackgroundAndNotify];
// Start the task
[theTask launch];
// Wait until it is finished
[theTask waitUntilExit];
// Wait one more second so that we can process any remaining output from the tool
NSDate *endDate = [NSDate dateWithTimeIntervalSinceNow:1];
while ([NSDate.date compare:endDate] == NSOrderedAscending) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
[[NSNotificationCenter defaultCenter] removeObserver:outputFileHandleReadCompletionObserver];
NSLog(#"Lines processed: %d", lineCount);
}
It's quite simple. In the observer block when data.length is 0 remove the observer and call terminate.
The code will continue after the waitUntilExit line.
- (void)runTool
{
// Retrieve 20000 lines of text by invoking `head -n 20000 /usr/share/dict/words`
const int expected = 20000;
NSTask *theTask = [[NSTask alloc] init];
theTask.qualityOfService = NSQualityOfServiceUserInitiated;
theTask.launchPath = #"/usr/bin/head";
theTask.arguments = #[#"-n", [#(expected) stringValue], #"/usr/share/dict/words"];
__block int lineCount = 0;
__block bool finished = false;
NSPipe *outputPipe = [NSPipe pipe];
theTask.standardOutput = outputPipe;
NSFileHandle *outputFileHandle = outputPipe.fileHandleForReading;
NSString __block *prevPartialLine = #"";
[[NSNotificationCenter defaultCenter] addObserverForName:NSFileHandleReadCompletionNotification object:outputFileHandle queue:nil usingBlock:^(NSNotification * _Nonnull note)
{
// Read the output from the cmdline tool
NSData *data = [note.userInfo objectForKey:NSFileHandleNotificationDataItem];
if (data.length > 0) {
// go over each line
NSString *output = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSArray *lines = [[prevPartialLine stringByAppendingString:output] componentsSeparatedByString:#"\n"];
prevPartialLine = [lines lastObject];
NSInteger lastIdx = lines.count - 1;
[lines enumerateObjectsUsingBlock:^(NSString *line, NSUInteger idx, BOOL * _Nonnull stop) {
if (idx == lastIdx) return; // skip the last (= incomplete) line as it's not terminated by a LF
// now we can process `line`
lineCount += 1;
}];
} else {
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleReadCompletionNotification object:nil];
[theTask terminate];
finished = true;
}
[note.object readInBackgroundAndNotify];
}];
NSParameterAssert(outputFileHandle);
[outputFileHandle readInBackgroundAndNotify];
// Start the task
[theTask launch];
// Wait until it is finished
[theTask waitUntilExit];
// Wait until all data from the pipe has been received
while (!finished) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.0001]];
}
NSLog(#"Lines processed: %d (should be: %d)", lineCount, expected);
}
The problem with waitUntilExit is that it doesn't always behave the way one might think. The following is mentioned in the documenation:
waitUntilExit does not guarantee that the terminationHandler
block has been fully executed before waitUntilExit returns.
It appears this is precisely the problem you are having; it's a race condition. The waitUntilExit is not waiting long enough and the lineCount variable is reached before the NSTask completes. The solution would likely be to use a semaphore or dispatch_group, although it's unclear if you want to go that route — this is not an easy problem to resolve it seems.
*I experienced a similar issue from months back that still isn't resolved unfortunately.

How to predefine Apple Script constants or variables

Is it possible to predefine constant or variable for an AppleScript in cocoa application?
in other words is the function "addConstantToAppleScript" (used in the following code) definable?
addConstantToAppleScript("myText", "Hello!");
char *src = "display dialog myText";
NSString *scriptSource = [NSString stringWithCString:src];
NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:scriptSource];
NSDictionary *scriptError = [[NSDictionary alloc] init];
[appleScript executeAndReturnError:scriptError];
thanks.
If you want to prepend an NSDictionary of key/value pairs to the beginning of an NSString containing AppleScript you could use something like the following function. Personally I would do this as a category on NSString but you have asked for a function.
NSString *addConstantsToAppleScript(NSString *script, NSDictionary *constants) {
NSMutableString *constantsScript = [NSMutableString string];
for(NSString *name in constants) {
[constantsScript appendFormat:#"set %# to \"%#\"\n", name, [constants objectForKey:name]];
}
return [NSString stringWithFormat:#"%#%#", constantsScript, script];
}
This function converts the key/value pairs to AppleScript statements of the form set <key> to "<value>". These statements are then added to the front of the supplied script string. The resulting script string is then returned.
You would use the above function as follows:
// Create a dictionary with two entries:
// myText = Hello\rWorld!
// Foo = Bar
NSDictionary *constants = [[NSDictionary alloc ] initWithObjectsAndKeys:#"Hello\rWorld!", #"myText", #"Bar", #"Foo", nil];
// The AppleScript to have the constants prepended to
NSString *script = #"tell application \"Finder\" to display dialog myText";
// Add the constants to the beginning of the script
NSString *sourceScript = addConstantsToAppleScript(script, constants);
// sourceScript now equals
// set Foo to "Bar"
// set myText to "Hello\rWorld!"
// tell application "Finder" to display dialog myText
NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:sourceScript];

stringByReplacingOccurenceOfString fails

I'm trying to make a Mac OS X application that asks the user for a directory. I'm using an NSOpenPanel that gets triggered when the user presses a "Browse" button.
The problem is, [NSOpenPanel filenames] was deprecated, so now I'm using the URLs function. I want to parse out the stuff that's url related to just get a normal file path. So I tried fileName = [fileName stringByReplacingOccurrencesOfString:#"%%20" withString:#" "];, but that gave me an error:
-[NSURL stringByReplacingOccurrencesOfString:withString:]: unrecognized selector sent to instance 0x100521fa0
Here's the entire method:
- (void) browse:(id)sender
{
int i; // Loop counter.
// Create the File Open Dialog class.
NSOpenPanel* openDlg = [NSOpenPanel openPanel];
// Enable the selection of files in the dialog.
[openDlg setCanChooseFiles:NO];
// Enable the selection of directories in the dialog.
[openDlg setCanChooseDirectories:YES];
// Display the dialog. If the OK button was pressed,
// process the files.
if ( [openDlg runModal] == NSOKButton )
{
// Get an array containing the full filenames of all
// files and directories selected.
NSArray* files = [openDlg URLs];
// Loop through all the files and process them.
for( i = 0; i < [files count]; i++ )
{
NSString* fileName = (NSString*)[files objectAtIndex:i];
NSLog(#"%#", fileName);
// Do something with the filename.
fileName = [fileName stringByReplacingOccurrencesOfString:#"%%20" withString:#" "];
NSLog(#"%#", fileName);
NSLog(#"Foo");
[oldJarLocation setStringValue:fileName];
[self preparePopUpButton];
}
}
}
Interestingly enough, "Foo" never gets outputted to that console. It's like the method aborts at the stringByReplacigOccurencesOfString line.
If I remove that one line, the app will run and fill my text box with the string, just in URL form, which I don't want.
Your problem is that the NSArray returned by [NSOpenPanel URLs] contains NSURL objects, not NSString objects. You're doing the following cast:
NSString* fileName = (NSString*)[files objectAtIndex:i];
Since NSArray returns an id, there isn't any compile-time checking to make sure your cast makes sense, but you do get a runtime error when you try to send an NSString selector to what is actually an NSURL.
You could convert the NSURL objects to NSString and use your code mostly as-is, but there's no need for you to handle the URL decoding yourself. NSURL already has a method for retrieving the path portion which also undoes percent-encoding: path.
NSString *filePath = [yourUrl path];
Even if your code was dealing with just a percent-encoded NSString, theres stringByReplacingPercentEscapesUsingEncoding:.

Getting URL From beginSheetModalForWindow:

I'm using an OpenPanel to get a file path URL. This works:
[oPanel beginSheetModalForWindow:theWindow completionHandler:^(NSInteger returnCode)
{
NSURL *pathToFile = nil;
if (returnCode == NSOKButton)
pathToFile = [[oPanel URLs] objectAtIndex:0];
}];
This doesn't, resulting in an 'assignment of read-only variable' error:
NSURL *pathToFile = nil;
[oPanel beginSheetModalForWindow:theWindow completionHandler:^(NSInteger returnCode)
{
if (returnCode == NSOKButton)
pathToFile = [[oPanel URLs] objectAtIndex:0];
}];
return pathToFile;
In general, any attempt to extract pathToFile from the context of oPanel has failed. This isn't such a big deal for small situations, but as my code grows, I'm forced to stuff everything -- XML parsing, core data, etc -- inside an inappropriate region. What can I do to extract pathToFile?
Thanks.
This doesn't, resulting in an 'assignment of read-only variable' error:
NSURL *pathToFile = nil;
[oPanel beginSheetModalForWindow:theWindow completionHandler:^(NSInteger returnCode)
{
if (returnCode == NSOKButton)
pathToFile = [[oPanel URLs] objectAtIndex:0];
}];
return pathToFile;
Yes, because you're trying to assign to the copy of the pathToFile variable that gets made when the block is created. You're not assigning to the original pathToFile variable that you declared outside the block.
You could use the __block keyword to let the block assign to this variable, but I don't think this will help because beginSheetModalForWindow:completionHandler: doesn't block. (The documentation doesn't mention this, but there's no reason for the method to block, and you can verify with logging that it doesn't.) The message returns immediately, while the panel is still running.
So, you're trying to have your completion-handler block assign to a local variable, but your method in which you declared the local variable will probably have returned by the time block runs, so it won't be able to work with the value that the block left will leave in the variable.
Whatever you do with pathToFile should be either in the block itself, or in a method (taking an NSURL * argument) that the block can call.
you can also runModal after you begin the sheet you just need to make sure you end the sheet later. This way you don't have to bend to apple's will, it isn't deprecated and it should still work perfectly.
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
[openPanel beginSheetModalForWindow:window completionHandler:nil];
NSInteger result = [openPanel runModal];
NSURL *url = nil;
if (result == NSFileHandlingPanelOKButton)
{
url = [openPanel URL];
}
[NSApp endSheet:openPanel];
It seems a little bit like black magic coding but it does work.

Making an NSMutableString transformation without leaking memory?

I have this function within an iPhone project Objective C class.
While it's correct in terms of the desired functionality, after a few calls, it crashes into the debugger.
So I think it's a case of bad memory management, but I'm not sure where.
- (NSString *)stripHtml:(NSString *)originalText {
// remove all html tags (<.*>) from the originalText string
NSMutableString *strippedText = [[NSMutableString alloc] init];
BOOL appendFlag = YES;
for( int i=0; i<[originalText length]; i++ ) {
NSString *current = [originalText substringWithRange:NSMakeRange(i, 1)];
if( [current isEqualTo:#"<"] )
appendFlag = NO;
if( appendFlag )
[strippedText appendString:current];
if( [current isEqualTo:#">"] )
appendFlag = YES;
}
NSString *newText = [NSString stringWithString:strippedText];
[strippedText release];
return newText;
}
Every time you iterate over your for loop, you're allocating a new NSString. While these NSStrings are autoreleased, they won't actually be released until after all the processing of your last input is finished. In the meantime, you'll allocate a potentially infinite amount of memory. The solution is to create your own autorelease pool and drain it every trip through the for loop. It'll look something like this:
BOOL appendFlag = YES;
for( int i=0; i<[originalText length]; i++ ) {
NSAutoreleasePool *pool = [NSAutoreleasePool new];
// rest of for loop body
[pool drain];
}
That'll free up the memory used by your current pointer right away.

Resources