/* windex.c ** Author: Ronald L. Rivest ** Helpful modifications from Carl Manning, Ron Greenberg. ** Documentation: see windex.doc ** Version: 1/26/90 ** Basic functionality: ** (1) input the file windex.tex into your tex source ** (2) put index entries into your Tex document (e.g. foo.tex) ** These are written out into file foo.idx by Tex. ** (3) Run `windex foo' to compile index. ** Output is now in file foo.index ** (4) Include in foo.tex the line ** \makeatletter\@input{book.index}\makeatother ** where you want the index to go. */ #include #include #include #include /* Miscellaneous constants */ #define TRUE 1 #define FALSE 0 #define DEBUG 0 #define CR '\r' #define LF '\n' #define EOS 0 /* end of string */ /* WINDEX constants */ /* String constants */ #define WINDEX_BANNER "WINDEX index-creation program. Version 1/26/90.\n" #define INPUT_EXTENSION ".idx" #define OUTPUT_EXTENSION ".index" #define MAX_STRING_LENGTH 300 #define DEFAULT_FORMAT "0" /* just print the page number */ char NULL_FORMAT[] = ""; /* null format string */ #define NULL_SORT_CHARS "-" /* nonblank characters to ignore when sorting */ #define UNKNOWN -1 /* Character constants: changing these constants changes syntax of windex */ #define SLASH '/' /* separates levels in sort key */ #define LANGLE '<' /* marks entry to start range */ #define RANGLE '>' /* marks entry to end range */ #define LBRACE '{' /* marks start of print value */ #define RBRACE '}' /* marks end of print value */ #define LBRACKET '[' /* marks start of format string */ #define RBRACKET ']' /* marks end of format string */ #define ZERO '0' /* marks position of page or range in format */ #define COLON ':' /* marks beginning of page number */ #define BLANK ' ' /* whitespace to ignore at beginning */ /* Windex policy switches */ #define MAKE_RANGES TRUE /* create ranges for adjacent page entries */ #define IGNORE_SMALL_WORDS TRUE /* ignore initial small words in subitems */ /* when sorting them into order */ /* TEX constants */ #define TEX_BEGIN_GROUP '{' #define TEX_END_GROUP '}' #define TEX_EMPTY_GROUP "{}" #define TEX_BEGIN_INDEX "\\begin{theindex}\n" #define TEX_END_INDEX "\n\n\\end{theindex}\n" #define TEX_MAX_LEVEL 2 #define TEX_BEGIN_LEVEL0 "\n\\item " #define TEX_BEGIN_LEVEL1 "\n \\subitem " #define TEX_BEGIN_LEVEL2 "\n \\subsubitem " #define TEX_AFTER_ENTRY ", " /* string to print after print value */ #define TEX_BETWEEN_PAGES ", " /* string to print between page entries */ #define TEX_BETWEEN_RANGE "--" /* end of TEX constants */ FILE *instream; /* lines as produced by \index commands */ FILE *outstream; /* actual latex commands to make index */ typedef /* for a page or a range of pages */ struct pageref { int level; /* 0...TEX_MAX_LEVEL */ char *sortkey[TEX_MAX_LEVEL+1]; /* values to sort/compare keys by */ char *pv; /* print value or else NULL */ char *format; /* format or replacement for page reference */ int firstpageno; /* first page number in range or UNKNOWN */ int lastpageno; /* last page number in range or UNKNOWN */ struct pageref *next; /* link to next page reference */ } entry; entry elist; entry *elast; /* mymalloc(n) ** allocate n bytes but check for out of memory condition */ char *mymalloc(n) int n; { char *p = malloc((unsigned)n); if (p == NULL) { printf("\nERROR: Windex ran out of memory.\n"); exit(0); } return(p); } /* mystrdup(s) ** copy string s but check for out of memory condition */ char *mystrdup(s) char *s; { char *p = mymalloc(strlen(s)+1); strcpy(p,s); return(p); } /* uppercase(c) ** convert character to uppercase */ char uppercase(c) char c; { if (islower(c)) return(toupper(c)); return(c); } /* printspage(s) ** TRUE if s is a format string that contains a spot to print a pageno */ printspage(s) char *s; { if (*s == EOS) return(FALSE); if (*s == ZERO) return(TRUE); return(printspage(s+1)); } /* sortpageno(e) ** return page to sort entry e by */ sortpageno(e) entry *e; { if (e->firstpageno != UNKNOWN) return(e->firstpageno); return(e->lastpageno); } /* entrylesseq(e1,e2,format_first) ** Returns TRUE if entry e1 should come before (or is equal to) entry e2 ** in the sorted list of entries. This is the basic comparison operation ** for sorting the entries into order. ** if format_first == TRUE then order by key, format, then page ** if format_first == FALSE then order by key, page, then format */ entrylesseq(e1,e2,format_first) entry *e1,*e2; int format_first; { int i,r; /* if e1 doesn't exist, then e2 comes first */ if (e1 == NULL) return(FALSE); /* if e2 doesn't exist, then e1 comes first */ if (e2 == NULL) return(TRUE); /* compare sort keys and return TRUE if e1 sorts first, ** FALSE if e2 sorts first. */ for (i=0;i<=e1->level && i<=e2->level;i++) { r = keycmp(i,e1,e2); if (r<0) return(TRUE); if (r>0) return(FALSE); /* level i keys are the same */ if (e1->level == i && e2->level > i) return(TRUE); if (e1->level > i && e2->level == i) return(FALSE); } /* The sort keys are the same. */ if (format_first) { r = strcmp(e1->format,e2->format); if (r<0) return(TRUE); if (r>0) return(FALSE); /* formats are the same */ if (sortpageno(e1) <= sortpageno(e2)) return(TRUE); return(FALSE); } else /* sort by pageno, then format */ { if (sortpageno(e1)sortpageno(e2)) return(FALSE); /* sort pages are the same */ if (strcmp(e1->format,e2->format)<=0) return(TRUE); return(FALSE); } } /* skipspace(p) ** advance p beyond any space characters (space, tabs, CRs, etc.) */ char *skipspace(p) char *p; { while (*p != EOS && isspace(*p)) p++; return(p); } /* skipnonsortletters(p) ** advance p beyond any non-sorting letters: whitespace or '-'s. */ char *skipnonsortletters(p) char *p; { char *q; if (*p == EOS) return(p); if (isspace(*p)) return(skipnonsortletters(p+1)); q = NULL_SORT_CHARS; while (*q && *p != *q) q++; if (*q) return(skipnonsortletters(p+1)); return(p); } char *smallword[] = { "a", "about", "also", "an", "and", "as", "at", "by", "of", "for", "from", "in", "into", "on", "onto", "the", "to", "with", "***" /* end marker */ }; /* skipsmallwords(p) ** advances p over any small words in the small word list. ** capitalization is ignored. ** small word must be followed by a blank */ char *skipsmallwords(p) char *p; { int i; char *pp, *q; p = skipnonsortletters(p); for (i=0;strcmp("***",smallword[i]);i++) { q = smallword[i]; pp = p; while (*pp && *q && uppercase(*pp) == uppercase(*q)) { pp++; q++; }; if (*q==EOS && isspace(*pp)) return(skipsmallwords(pp)); } return(p); } /* keycmp(i,e1,e2) ** compare e1->sortkey[i] with e2->sortkey[i] ** return -1 if e1 sorts first ** 0 if they are the same ** 1 if e2 sorts first */ keycmp(i,e1,e2) int i; entry *e1, *e2; { char *p1, *p2; int r; /* get sort keys */ p1 = e1->sortkey[i]; p2 = e2->sortkey[i]; /* ignore initial small words when sorting subitems */ if (i>0 && IGNORE_SMALL_WORDS) { p1 = skipsmallwords(p1); p2 = skipsmallwords(p2); } /* find position of their first difference, if any (ignoring case, ** blanks, hyphens) */ p1 = skipnonsortletters(p1); p2 = skipnonsortletters(p2); while (*p1!=EOS && *p2!=EOS && uppercase(*p1) == uppercase(*p2)) { p1 = skipnonsortletters(p1+1); p2 = skipnonsortletters(p2+1); } if ( *p1 == EOS && *p2 != EOS ) return(-1); /* e1 a prefix of e2 --> -1 */ if ( *p2 == EOS && *p1 != EOS ) return(1); /* e2 a prefix of e1 --> FALSE */ /* sort alphabetically by uppercase version of letters, if possible */ if (uppercase(*p1) < uppercase(*p2)) return(-1); if (uppercase(*p2) < uppercase(*p1)) return(1); /* now the uppercase version of the entries are known to be the same */ /* redo the comparison, paying attention to case */ /* sort upper case entry AFTER lower case entry */ /* get sort keys */ p1 = e1->sortkey[i]; p2 = e2->sortkey[i]; while (*p1!=EOS && *p2!=EOS && *p1 == *p2) { p1 = skipnonsortletters(p1+1); p2 = skipnonsortletters(p2+1); } /* sort alphabetically by differing cases of same letter, if possible */ if (isupper(*p1) && islower(*p2)) return(1); if (isupper(*p2) && islower(*p1)) return(-1); /* keys are totally identical */ /* try to sort by blanks and hyphens ??? */ r = strcmp(e1->sortkey[i],e2->sortkey[i]); if (r!=0) printf("\n----- WARNING: Keys `%s' and `%s' are almost identical.\n", e1->sortkey[i],e2->sortkey[i]); return(r); } /* mergelists(e1,e2,format_first) ** This is the basic merge operation for a merge-sort. ** The entries e1 and e2 are the heads of two disjoint lists of entries. ** The result is the merged list of entries. ** The lists are destructively modified in this merge by modification ** of the `next' entries. ** The `entrylesseq' operator is used for comparisons between entries. ** if format_first == TRUE then merge by key, format, then page ** if format_first == FALSE then merge by key, page, then format */ entry *mergelists(e1,e2,format_first) entry *e1, *e2; int format_first; { entry head, *last = &head; head.next = NULL; while (TRUE) { if (e1 == NULL) { last->next = e2; return(head.next); } if (e2 == NULL) { last->next = e1; return(head.next); } if (entrylesseq(e1,e2,format_first)) { if (DEBUG) { printf("\n ["); printsortkey(e1); printf("] <= ["); printsortkey(e2); printf("]"); } last->next = e1; last = e1; e1 = e1->next; } else { if (DEBUG) { printf("\n ["); printsortkey(e1); printf("] > ["); printsortkey(e2); printf("]"); } last->next = e2; last = e2; e2 = e2->next; } } } /* sortlist(e,format_first) ** This a the merge-sort operator on list e of entries. ** The sort is a ``stable sort'', so entries with the same key stay ** in the same relative order. ** if format_first == TRUE then sort by key, format, then page ** if format_first == FALSE then sort by key, page, then format */ entry *sortlist(e,format_first) entry *e; int format_first; { entry *f,*g; int i,len; if (e == NULL || e->next == NULL) return(e); if (DEBUG) { printf("\n\nSorting:"); printlist(e); } /* let f point to entry half-way down list */ f = e; len = 0; while (f != NULL) {len++; f = f->next;} for (f=e,i=0; inext) ; /* now split list into two */ g = f->next; f->next = NULL; /* recurse and merge */ e = sortlist(e,format_first); /* first half of list */ g = sortlist(g,format_first); /* second half of list */ e = mergelists(e,g,format_first); if (DEBUG) { printf("\n\nSorting result:"); printlist(e); } return(e); } /* needsparent(e, f) ** Here e and f are entries in the sorted list: f = e->next. ** The question is whether or not an entry should be added to precede f ** in the list, because f is a multi-level entry but no entry for the super- ** entries are present. Since e is the predecessor of f in the list, we ** don't need to add a parent (superentry) if ** f is at level 0 ** (e.g. f = "FOO"), or ** The f->level - 1 first keys of e are the same as f's: ** (e.g. e = "A/B", f = "A/B/C"), ** (e.g. e = "A/B/C/D", f = "A/B/E") */ needsparent(e, f) entry *e, *f; { int i; if (f->level == 0) return(FALSE); /* e is the prefix of f --> FALSE */ for (i=0;i<=f->level-1;i++) if (strcmp(e->sortkey[i],f->sortkey[i])) return(TRUE); return(FALSE); } /* addparentifnecessary(e) ** Add a new entry after e if the entry f following e is multi-level and ** it's superentries are not already in list. This can be determined ** by examining e and f together, assuming the list entries are processed ** in order. */ addparentifnecessary(e) entry *e; { entry *f = e->next; int i; if (samesortkey(e,f)) return; /* same sortkey --> do nothing */ if (f->level == 0) return; /* f at level 0 --> do nothing */ if (needsparent(e,f)) { /* create a new entry for f's parent */ entry *g = (entry *)mymalloc(sizeof(entry)); g->level = f->level - 1; for (i=0;i<=TEX_MAX_LEVEL;i++) g->sortkey[i] = NULL; for (i=0;i<=g->level;i++) g->sortkey[i] = mystrdup(f->sortkey[i]); g->pv = NULL; g->format = NULL_FORMAT; /* no page numbers for super-entries */ g->firstpageno = 0; g->lastpageno = 0; g->next = f; /* splice g into list: e-->g-->f */ e->next = g; addparentifnecessary(e); /* redo in case g now needs a parent */ } } /* addparents() ** add all superentries that are needed to the entry list `elist', ** using the `addparentifnecessary' routine on each entry. */ addparents() { entry *e = &elist; while (e->next != NULL) { addparentifnecessary(e); e = e->next; } } /* samesortkey(e,f) ** returns TRUE if and only if entries e and f have the same sort key */ samesortkey(e,f) entry *e, *f; { int i; if (e->level != f->level) return(FALSE); for (i=0;i<=e->level;i++) if (strcmp(e->sortkey[i],f->sortkey[i])) return(FALSE); return(TRUE); } /* spliceout(e,f) ** splice out the entry f. e is f's predecessor. */ spliceout(e,f) entry *e,*f; { e->next = f->next; /* save print value of f if e doesn't have a print value */ if (e->pv == NULL && f->pv != NULL) e->pv = f->pv; else if (e->pv != NULL && f->pv != NULL && strcmp(e->pv,f->pv) && strcmp(f->pv,f->sortkey[f->level]) ) { printf("\n----- WARNING: Print value %s overridden for entry:\n",f->pv); printentry(f); printf("\n"); } /* save format string (assumption: e's is the same or NULL_FORMAT */ e->format = f->format; } /* compressentrylist() ** Loop through list `elist' of index entries, and combine adjacent ** entries to make page ranges whenever possible. */ compressentrylist() { entry *e = elist.next,*f; while (e != NULL && e->next != NULL) { f = e->next; if (!samesortkey(e,f)) { e=f; /* can't combine, move on to f */ } else if (e->format == NULL_FORMAT) /* Note NULL_FORMATs sort earlier */ { spliceout(e,f); e->firstpageno = f->firstpageno; e->lastpageno = f->lastpageno; } else if (strcmp(e->format,f->format)) { /* different formats */ e=f; /* can't combine, move on to f */ } else if (e->lastpageno == UNKNOWN && f->firstpageno == UNKNOWN) { spliceout(e,f); /* combine start and end of range */ e->lastpageno = f->lastpageno; /* reprocess e */ } else if (e->lastpageno == UNKNOWN && f->firstpageno != UNKNOWN) { spliceout(e,f); /* delete page in middle of range */ } else if (f->firstpageno == UNKNOWN) { /* ERROR -- no start of range */ e = f; /* (will be reported later) */ } else if ( e->lastpageno == f->firstpageno || e->lastpageno +1 == f->firstpageno) { spliceout(e,f); /* remove f */ e->lastpageno = f->lastpageno; /* reprocess e */ } else { e = f; /* process f next */ } } } /* main(argc,argv) ** requires one command-line argument: the name of the file (no extension) */ main(argc,argv) int argc; char **argv; { char fname[MAX_STRING_LENGTH]; int i; /* If no arguments given, instruct user */ if (argc == 1) { printf("\nUsage: windex filename\n"); exit(0); } /* print banner */ printf(WINDEX_BANNER); /* Open input and output files */ strcpy(fname,argv[1]); strcat(fname,INPUT_EXTENSION); /* input file = foo.idx */ instream = fopen(fname,"r"); if (instream == NULL) { printf("\nERROR: input file `%s' can not be opened.\n",fname); exit(0); } strcpy(fname,argv[1]); strcat(fname,OUTPUT_EXTENSION); /* output file = foo.index */ outstream = fopen(fname,"w"); if (outstream == NULL) { printf("\nERROR: output file `%s' can not be opened.\n",fname); exit(0); } /* Initialize entry list to one entry, consisting of a null string */ /* (The null string should sort first) */ elist.level = 0; elist.next = NULL; for (i=0;isortkey[0]); if (e->level>=1) printf("/%s",e->sortkey[1]); if (e->level>=2) printf("/%s",e->sortkey[2]); } /* printentry(e) ** print out the entry e in form similar to input form. */ printentry(e) entry *e; { printf("\n"); if (e==NULL) printf("NULL"); printsortkey(e); if (e->pv != NULL) printf("%s",e->pv); /* else printf("{%s}",e->sortkey[e->level]); */ if (e->lastpageno == UNKNOWN) printf("%c",LANGLE); if (e->firstpageno == UNKNOWN) printf("%c",RANGLE); if (e->format != NULL) printf("[%s]",e->format); printf(":%d",sortpageno(e)); fflush(stdout); } /* printlist(e) ** print out the list of entries starting at e */ printlist(e) entry *e; { while (e != NULL) { printentry(e); e = e->next; } } /* inputfile() ** read the input file a line at a time and process each line. ** print a warning if any line begins with ``\indexentry{'' */ inputfile() { char buffer[MAX_STRING_LENGTH]; int oldstylewarn; /* flag: only give the warning once */ oldstylewarn = FALSE; while (NULL != fgets(buffer,MAX_STRING_LENGTH-1,instream)) { if ((!oldstylewarn) && oldstyle(buffer)) { oldstylewarn = TRUE; printf("\n----- WARNING: You appear not to have \\input windex.tex."); printf("\n Proceeding anyway.\n"); }; inputline(buffer); } } /* oldstyle(buffer) ** true if buffer starts with "\indexentry{" */ oldstyle(buffer) char *buffer; { int i = 0; while ((buffer[i] == "\\indexentry{"[i]) && (++i < 12)) ; return(i == 12); } /* inputline(buffer) ** Scan an input line and create an entry for it. */ inputline(buffer) char *buffer; { char key[MAX_STRING_LENGTH]; /* sort key */ char pv[MAX_STRING_LENGTH]; /* print value */ char format[MAX_STRING_LENGTH]; /* format string for page number */ char *bufferp,*keyp,*pvp; entry *e; int pvseen; /* flag if print-value has been seen */ int i,pageno; /* Create a new empty entry for this line */ e = (entry *)mymalloc(sizeof(entry)); e->level = 0; for (i=0;isortkey[i] = NULL; e->pv = NULL; e->format = DEFAULT_FORMAT; e->firstpageno = 0; e->lastpageno = 0; /* scan line */ bufferp = buffer; /* input buffer pointer */ keyp = key; /* sort key output pointer */ key[0] = EOS; /* sort key is null string */ pvp = pv; /* print value output pointer */ pvseen = FALSE; /* print value not seen yet */ strcpy(format,DEFAULT_FORMAT); while (*bufferp==BLANK) bufferp++; /* skip leading blanks */ while (*bufferp && *bufferp != CR && *bufferp != LF) { switch (*bufferp) { case LANGLE: /* LANGLE (<) is flag to start a page range */ e->lastpageno = UNKNOWN; bufferp++; break; case RANGLE: /* RANGLE (>) is flag to end a page range */ e->firstpageno = UNKNOWN; bufferp++; break; case LBRACE: /* LBRACE ({) marks beginning of print value string */ bufferp = scanbalance(bufferp,pv); /* copy string into pv */ pvp = pv + strlen(pv); pvseen = TRUE; break; case LBRACKET: /* LBRACKET ([) marks start of format string */ bufferp = scanbalance(bufferp,format); break; case COLON: /* COLON (:) marks beginning of page number */ pageno = atoi(++bufferp); if (e->firstpageno != UNKNOWN) e->firstpageno = pageno; if (e->lastpageno != UNKNOWN) e->lastpageno = pageno; goto saveline; /* and break out of loop */ case SLASH: /* SLASH marks superentry divisions */ bufferp++; /* skip over the slash */ *keyp = EOS; /* terminate sort key */ e->sortkey[e->level] = mystrdup(key); keyp = key; e->level = e->level + 1; if (e->level > TEX_MAX_LEVEL) printf("\n----- WARNING: Too many levels in entry: %s", buffer); break; default: /* Copy character into sort key */ *keyp++ = *bufferp++; } } saveline: /* If we haven't seen a sort key, this is probably a blank line. ** Don't save this entry on the list. */ if (strlen(key)==0) return; /* Terminate sort key */ *keyp = EOS; /* Copy sort key into newly allocated string */ e->sortkey[e->level] = mystrdup(key); /* Copy print value into newly allocated string */ if (pvseen) e->pv = mystrdup(pv); /* Copy format into newly allocated string, if format exists */ if (strcmp(format,TEX_EMPTY_GROUP)==0) /* empty format means no page */ { e->format = NULL_FORMAT; /* (used for super-entry) */ } else if (format[0] != EOS) { e->format = mystrdup(format); } /* If format doesn't contain a spot for pageno, then make pageno = 0 */ /* so it sorts at front */ if (!printspage(e->format)) e->firstpageno = e->lastpageno = 0; /* Append new entry to end of list. elast points to end of list */ e->next = NULL; elast->next = e; elast = e; if (DEBUG) printentry(e); } /* printvalueaux(f,e) ** returns NULL if sort key of e is the different than f's ** otherwise returns printvalue of e, if it exists ** otherwise returns printvalueaux of e's successor */ char *printvalueaux(f,e) entry *f, *e; { if (e == NULL) return(NULL); if (!samesortkey(f,e)) return(NULL); if (e->pv != NULL) return(e->pv); return(printvalueaux(f,e->next)); } /* printvalue(f) ** returns print value for entry f, or sort key if no print value given ** checks succeeding entries with same sort key to see if any of them ** have a print value given, and if so, uses that */ char *printvalue(f) entry *f; { char *pv; pv = printvalueaux(f,f); if (pv != NULL) return(pv); return(f->sortkey[f->level]); } /* makeindex() ** Runs through the list elist, and prints out the output file foo.index. */ makeindex() { entry *e = &elist; entry *f; int n = 0; /* number of entries printed */ int entry_printed_last = FALSE; /* TRUE if entry printed last, FALSE if page printed last */ char *q; if (e->next == NULL) { printf("No output produced (no entries found in input file).\n",n); return; } fprintf(outstream,TEX_BEGIN_INDEX); while (e->next != NULL) { f = e->next; /* entry to print is f */ n++; /* Check for unbalanced page ranges and print warnings */ if (f->lastpageno == UNKNOWN) { printf("\n----- WARNING: closing entry not found to match following starting %c entry:",LANGLE); printentry(f); printf("\n"); } if (f->firstpageno == UNKNOWN) { printf("\n----- WARNING: starting entry not found to match following ending %c entry:",RANGLE); printentry(f); printf("\n"); } /* Print entry text if necessary */ if (!samesortkey(e,f)) { /* Key of f is different than key of predecessor e */ /* print appropriately indented entry with subitems, etc. */ if (f->level == 0) fprintf(outstream,TEX_BEGIN_LEVEL0); else if (f->level == 1) fprintf(outstream,TEX_BEGIN_LEVEL1); else if (f->level == 2) fprintf(outstream,TEX_BEGIN_LEVEL2); /* print ``print value'' for f */ fprintf(outstream,"%s",printvalue(f)); entry_printed_last = TRUE; } /* print TEX_AFTER_ENTRY or TEX_BETWEEN_PAGES if necessary */ if (strcmp(f->format,NULL_FORMAT)==0) { ; /* no page entry to be printed, so no separator */ } else if (entry_printed_last) fprintf(outstream,TEX_AFTER_ENTRY); else if (entry_printed_last == FALSE) fprintf(outstream,TEX_BETWEEN_PAGES); /* Print page number in its format */ if (strcmp(f->format,NULL_FORMAT)) { /* check for no page number printed */ if (!printspage(f->format)) fprintf(outstream,"%s",f->format); else /* insert page or page range */ { /* print first part of format */ q = f->format; while (*q && *q != ZERO) { fprintf(outstream,"%c",*q); q++; } /* print first page number */ fprintf(outstream,"%d",f->firstpageno); /* if necessary, print range */ if (f->lastpageno != f->firstpageno) { fprintf(outstream,"%s",TEX_BETWEEN_RANGE); fprintf(outstream,"%d",f->lastpageno); } q++; /* skip the zero */ while (*q) fprintf(outstream,"%c",*q++); } entry_printed_last = FALSE; } /* go on to next entry */ e = e->next; } fprintf(outstream,TEX_END_INDEX); printf("Done. %d entries processed.\n",n); }