/* lqkwic.c -- Copyright 1991, 1992, 1994, 1996 Liam R. E. Quin. * All Rights Reserved. * This code is NOT in the public domain. * See the file COPYRIGHT for full details. * * lqkwic -- produce a keyword-in-context list of matches... * Liam R. Quin, February 1991 and later... * * $Id: lqkwic.c,v 1.43 2001/05/31 03:50:13 liam Exp $ * * * lqkwic presents user with a keyword-in-context (KWIC) index, * by looking up each match in the file, getting the surrounding * text, and displaying it. * * lqkwic can generate various formatted output: * + text before the matches for a new file-name * + text before each match * + text after each match * + text between the components of a match * + specified amounts of context before and after the match * + specified portions of the file name * */ #include "error.h" #include #include #include "globals.h" /* defines and declarations for database filenames */ #ifdef HAVE_SYSV_FCNTL_H # include #endif #ifdef HAVE_FCNTL_H # include #endif /* curses files are for terminfo variables */ #ifdef CURSESX # include # include #endif #include #ifdef HAVE_STRING_H # include #else # include #endif #ifdef HAVE_STDLIB_H # include #else # include #endif #include "range.h" #include "fileinfo.h" #include "wordinfo.h" #include "wordrules.h" #include "emalloc.h" #include "lqutil.h" #include "liblqtext.h" #include "wmoffset.h" #include "lqtrace.h" #include "namespace.h" #include "chartype.h" /* for IS_UPPER etc., faster alternatives */ #ifdef HAVE_UNISTD_H # include #endif #ifdef HAVE_STDLIB_H # include #endif PRIVATE long fReadMatchFile( #ifdef HAVE_PROTO FILE *FileWithMatches, char *MatchFileName #endif ); PRIVATE char *GetVariableValue( #ifdef HAVE_PROTO char *VariableName, unsigned long FileNumber, long MatchNumber, unsigned long MatchWithinFile, t_FID FID, char *DocName, /* the name stored in the database */ char *FileName, /* the expanded file name */ char *LastDocName, /* the previous DocName */ int NumberOfWordsInPhrase, unsigned long BlockInFile, unsigned long WordInBlock, char *TextBefore, char *MatchedText, char *TextAfter, unsigned long StartByte, unsigned long EndByte #endif ); PRIVATE int ShowOneMatch( #ifdef HAVE_PROTO long MatchNumber, int WordCount, unsigned long BlockInFile, unsigned long WordInBlock, char *DocumentName, t_FID FID #endif ); PRIVATE char *EndsEntity( #ifdef HAVE_PROTO char *String, char *Buffer #endif ); PRIVATE char *StartsEntity( #ifdef HAVE_PROTO char *String #endif ); PRIVATE char *EntityValue( #ifdef HAVE_PROTO char *Name #endif ); PRIVATE void ReadEntityFile( #ifdef HAVE_PROTO char *FileName #endif ); PRIVATE void ListVariables( #ifdef HAVE_PROTO FUNCTION_WITHOUT_ARGUMENTS #endif ); PRIVATE int StringRefersToVariable( #ifdef HAVE_PROTO char *String, char *Variable #endif ); PRIVATE void OutputMatch( #ifdef HAVE_PROTO long MatchNumber, t_FID FID, char *DocName, /* the name stored in the database */ char *FileName, /* the expanded file name */ int NumberOfWordsInPhrase, unsigned long BlockInFile, unsigned long WordInBlock, char *TextBefore, char *MatchedText, char *TextAfter, unsigned long StartByte, unsigned long EndByte #endif ); /** **/ char *progname = "lqkwic"; /* set from argv[] in main() */ static char *Revision = "@(#) $Id: lqkwic.c,v 1.43 2001/05/31 03:50:13 liam Exp $"; PRIVATE int TruncateAtPath = 0; PRIVATE int MatchesHaveWordCount = 1; #define MATCHLEN (3 + MatchesHaveWordCount) /* Default string segment widths: * Use the screen width for the overall amount, dividing the * remaining space evenly between left and right: */ #define DEFAULT_SCREENWIDTH 75 /* in case we can't deduce it */ /* Default buffer sizes: */ PRIVATE int LeftWidth = DEFAULT_SCREENWIDTH * 2; PRIVATE int RightWidth = DEFAULT_SCREENWIDTH * 2; PRIVATE int GapWidth = 0; PRIVATE int unbufferedMode = 0; PRIVATE unsigned long LineNumber = 0L; PRIVATE char *theMatchFileFormat = "==== Document ${FileNumber}: ${FileName} ====${NEWLINE}"; PRIVATE char *theMatchFormat = "$[${MatchNumber/3r}:${TextBefore/30r 30l}${Gap}${MatchedText}${TextAfter}/79 79r]${NEWLINE}"; PRIVATE char *postMatchFileFormat = 0; PRIVATE int SuppressTags = 0; /* suppress SGML tags */ PRIVATE char TagMark = '/'; PRIVATE ConvertSGMLTagsToEntities = 0; PRIVATE t_Range *RangeOfMatchesToPrint = 0; PRIVATE t_Range *RangeOfFilesToPrint = 0; /* If there were no matches in the input, we normally simply exit * without doing anything, but if NiceWarnIfEmpty is set, we print * a message saying that there were no matches; */ PRIVATE NiceWarnIfEmpty = 0; /* characters used for variable expansion: */ #define BRA '{' #define KET '}' /* lazy evalyation flags */ typedef struct { int NeedText; int NeedFileName; } t_LazyEvaluation; PRIVATE t_LazyEvaluation LazyEvaluation = { 0, 0 }; PRIVATE t_LQTEXT_Database *db = 0; int main(argc, argv) int argc; char *argv[]; { extern int optind, getopt(); extern char *optarg; /* for getopt */ int ch; /* for getopt */ int ErrFlag = 0; /* see how getopt makes programs cleaner? */ char *FileWithMatches = (char *) 0; int SeenMatches = 0; int NeedVariableList = 0; long PrintedMatches = 0; t_lqdbOptions *Options; { char *p = getenv("COLS"); if (p && *p) { if (!LQU_cknatstr(p)) { Error(E_WARN, "Environent variable $COLS has non-numeric value \"%s\"", p ); } else { int Width; Width = atoi(p); if (Width > 0 && Width != DEFAULT_SCREENWIDTH) { LeftWidth = Width / 2; RightWidth = (Width + 1) / 2; } } } } progname = argv[0]; Options = LQT_InitFromArgv(argc, argv); /* All lq-text programs must call LQT_InitFromArgv() before getopt, and * must then be prepared to ignore options z with arg and Z without. */ while ((ch = getopt(argc, argv, "A:C:ceE:f:g:i:Ll:Mo:O:p:r:S:s:uVvxz:Z")) != EOF) { switch (ch) { case 'Z': case 'z': break; /* done by LQT_InitFromArgv(); */ case 'V': fprintf(stderr, "%s version %s\n", progname, Revision); break; case 'A': postMatchFileFormat = optarg; break; case 'c': SuppressTags = 1; break; case 'C': TagMark = optarg[0]; break; case 'e': NiceWarnIfEmpty = 1; break; case 'g': if (!LQU_cknatstr(optarg)) { Error(E_FATAL|E_USAGE|E_XHINT, "-g must be followed by a whole number, not \"%s\"", optarg ); } GapWidth = atoi(optarg); break; case 'E': ReadEntityFile(optarg); break; case 'f': FileWithMatches = optarg; break; case 'i': ConvertSGMLTagsToEntities = 1; break; case 'l': LeftWidth = atoi(optarg); break; case 'L': NeedVariableList = 1; break; case 'M': MatchesHaveWordCount = 0; break; case 'o': RangeOfMatchesToPrint = LQU_StringToRange(optarg); break; case 'O': RangeOfFilesToPrint = LQU_StringToRange(optarg); break; case 'p': TruncateAtPath = atoi(optarg); if (!TruncateAtPath) { Error(E_FATAL|E_XHINT|E_USAGE, "-p: must be followed by number of / characters to delete" ); } break; case 'r': RightWidth = atoi(optarg); break; case 'S': theMatchFileFormat = optarg; break; case 's': theMatchFormat = optarg; break; case 'u': unbufferedMode= 1; break; case 'v': if (LQT_SetTraceFlag(LQTRACE_VERBOSE)) { /* it was already set */ (void) LQT_SetTraceFlag(LQTRACE_DEBUG); } break; case 'x': ErrFlag = (-1); break; default: Error(E_WARN, "option -%c is not recognised by this program", ch ); case '?': ErrFlag = 1; } } if (ErrFlag < 0) { /* -x or -xv was used */ fprintf(stderr, "usage: %s [-xv] [options] [matches...]\n", progname); fprintf(stderr, "use %s -x, -xv or -xvv for more detailed explanations.\n", progname); if (LQT_TraceFlagsSet(LQTRACE_VERBOSE|LQTRACE_DEBUG)) { fprintf(stderr, "\n\ -c -- suppress SGML flags\n\ -C char -- replace SGML tags with `char'\n\ -e -- an empty match file indicates no matches were found\n\ -E file -- \"file\" contains SGML entity names and values\n"); fprintf(stderr, "\ The format of the entity file is:\n\ # blank lines and comments are ignored;\n\ # comments start with a # and go on the end of the line.\n\ name \"replacement value\"\n\ # name is the name of an entity. Case is significant.\n\ # the replacement value must be quoted if it contains\n\ # any spaces; you can use \"...\", `...' or '...'\n\ # Entities must be marked as &name; in the text."); fprintf(stderr, "\ -f file -- \"file\" contains a list of matches, one per line\n\ -l n -- display n characters to the left of each phrase [%d]\n\ -L -- list variables available for -s and -S\n\ -M -- matches do not contain a word count\n\ -o range - print only matches falling within the given range\n\ -O range - print only matches in documents within the given range\n", LeftWidth ); if (LQT_TraceFlagsSet(LQTRACE_VERBOSE|LQTRACE_DEBUG)) { fprintf(stderr, "\ The format of a range is:\n\ -12,14-16,130-200,301,400 417 796,1003,1800-\n\ where a leading -12 means to print everything up to and\n\ including the twelth item;\n\ a trailing 1800- means to print item 1800 and following;\n\ 14-27 means to print items 14, 15 and 16;\n\ the other numbers standing for themselves;\n\ commas (,) and spaces are interchangeable at pleasure.\n\n" ); } fprintf(stderr, "\ -p n -- truncate pathnames at the nth rightmost `/' [%d]\n\ -r n -- display r chars to the right of each phrase's start [%d]\n", TruncateAtPath, RightWidth ); fprintf(stderr, "\ -s fmt -- use the given format to display each match\n\ -S fmt -- use the given format to display each file name\n\ (-v -L lists available variables and default values)\n -u --unbuffered mode; flush output after each result\n" ); } if (LQT_TraceFlagsSet(LQTRACE_VERBOSE|LQTRACE_DEBUG)) { fputs("\ Matches should be in the form of\n\ NumberOfWordsInPhrase BlockNumber WordInBlock FileName\n\ (This format is produced by lqrank, lqword -l, and lqphrase;\n\ the -M option indicates that NumberOfWordsInPhrase is omitted.)\n\ ", stderr); } LQT_PrintDefaultUsage(Options); if (NeedVariableList) { ListVariables(); } exit(0); } else if (ErrFlag > 0) { fprintf(stderr, "use %s -x for an explanation.\n", progname); exit(1); } if (NeedVariableList) { ListVariables(); exit(0); } db = LQT_OpenDatabase(Options, O_RDONLY, 0); if (!db || LQT_ObtainReadOnlyAccess(db) < 0) { Error(E_FATAL, "unable to open lq-text database in directory \"%s\"", db->DatabaseDirectory ); } /* Lazy Evaluation: * We don't need to look at the contents of each file if we're * not going to print out anything that depends on having it. * A cleaner way to do this would involve building up a representation * of the information on which the individual variables depended, * but it's hard to do that efficiently. */ if (theMatchFileFormat && *theMatchFileFormat) { LazyEvaluation.NeedText = ( /* these are in guessed order of likelihood... * there's another two lists like this on the next screen, * watch out! */ StringRefersToVariable(theMatchFileFormat, "TextBefore") || StringRefersToVariable(theMatchFileFormat, "TextAfter") || StringRefersToVariable(theMatchFileFormat, "MatchedText") || StringRefersToVariable(theMatchFileFormat, "StartByte") || StringRefersToVariable(theMatchFileFormat, "EndByte") || StringRefersToVariable(theMatchFileFormat, "LeftPad") ); LazyEvaluation.NeedFileName = ( StringRefersToVariable(theMatchFileFormat, "FileName") ); } if (!LazyEvaluation.NeedText) { if (postMatchFileFormat && *postMatchFileFormat) { LazyEvaluation.NeedText = ( /* these are in guessed order of likelihood... * there's another two lists like this above & below, * watch out! */ StringRefersToVariable(postMatchFileFormat, "TextBefore") || StringRefersToVariable(postMatchFileFormat, "TextAfter") || StringRefersToVariable(postMatchFileFormat, "MatchedText") || StringRefersToVariable(postMatchFileFormat, "StartByte") || StringRefersToVariable(postMatchFileFormat, "EndByte") || StringRefersToVariable(postMatchFileFormat, "LeftPad") ); } } if (postMatchFileFormat && !LazyEvaluation.NeedFileName) { LazyEvaluation.NeedFileName = ( StringRefersToVariable(postMatchFileFormat, "FileName") ); } if (theMatchFormat && *theMatchFormat) { if (!LazyEvaluation.NeedText ) { /* there's another two lists like this above & below, * watch out! */ LazyEvaluation.NeedText = ( StringRefersToVariable(theMatchFormat, "TextBefore") || StringRefersToVariable(theMatchFormat, "TextAfter") || StringRefersToVariable(theMatchFormat, "MatchedText") || StringRefersToVariable(theMatchFormat, "StartByte") || StringRefersToVariable(theMatchFormat, "EndByte") || StringRefersToVariable(theMatchFormat, "LeftPad") ); } if (!LazyEvaluation.NeedFileName) { LazyEvaluation.NeedFileName = ( StringRefersToVariable(theMatchFormat, "FileName") ); } } /* check that we can get at the file containing the matches, if one * was supplied. */ if (FileWithMatches) { long NumberOfMatches; if (*FileWithMatches == '-' && FileWithMatches[1] == '\0') { NumberOfMatches = fReadMatchFile(stdin, "standard input"); if (NumberOfMatches < 0) { Error(E_FATAL|E_SYS, "couldn't read matches from standard input" ); } else if (NumberOfMatches > 0) { PrintedMatches = 1; } ++SeenMatches; } else { FILE *f = LQU_fEopen( E_FATAL, FileWithMatches, "list of matches", "r" ); /* Now read the file, and make an array of matches... */ NumberOfMatches = fReadMatchFile(f, FileWithMatches); if (NumberOfMatches < 0) { Error(E_FATAL|E_SYS, "couldn't read matches from %s", FileWithMatches ); } else { if (NumberOfMatches > 0) { PrintedMatches = 1; } ++SeenMatches; } (void) fclose(f); } } argv += optind; argc -= optind; if (SeenMatches) { if (argc > 0) { Error(E_FATAL|E_USAGE|E_XHINT, "unknown argument %s", *argv ); } } else { long NumberOfMatches; NumberOfMatches = fReadMatchFile(stdin, "Standard input"); if (NumberOfMatches < 0) { Error(E_FATAL|E_SYS, "couldn't read matches from standard input" ); } else if (NumberOfMatches > 0) { PrintedMatches = 1; ++SeenMatches; } } if (SeenMatches && !PrintedMatches) { if (NiceWarnIfEmpty) { printf("No matches were found.\n"); } } return 0; } #define GLUE_START '{' #define GLUE_STOP '}' #define GLUE_ERROR '?' #define GLUE_STRING '"' #define GLUE_NUMBER '0' #define GLUE_ASSIGN '=' #define GLUE_NAME 'N' static t_NameSpace *glueNameSpace = 0; static t_NameSpaceTableEntry GlueTable[] = { { "DocumentMatches", LQU_NameType_String, 0 }, /** terminate the list: **/ { 0, } }; PRIVATE void MakeGlueNameSpace() { glueNameSpace = LQU_NameSpaceTableToNameSpace( "Glue Table", GlueTable ); if (!glueNameSpace) { Error(E_FATAL|E_INTERNAL, "couldn't create name space from glue table" ); } } static char *theTokenString = 0; PRIVATE int gettoken(Line) char *Line; { static char *oldLine = 0; static char *p; if (Line != oldLine) { oldLine = p = Line; } if (!Line) { return 0; } while (isspace(*p)) { p++; } if (!*p) { return GLUE_STOP; } theTokenString = p; if (*p == '{') { p++; return GLUE_START; } else if (*p == '}') { p++; return GLUE_STOP; } else if (*p == '"') { /* glue string */ theTokenString = ++p; while (*p && *p != '"') { p++; } if (!*p) { Error(E_WARN|E_MULTILINE, "Mistake in Glue expression:"); Error(E_WARN|E_MULTILINE|E_LASTLINE, "double quote (\") must be matched by \" on the same line" ); return GLUE_ERROR; } *p = '\0'; p++; return GLUE_STRING; } else if (isdigit(*p)) { do { p++; } while (*p && isdigit(*p)); if (*p) { *p = '\0'; p++; } return GLUE_NUMBER; } else if (*p == '=') { p++; return GLUE_ASSIGN; } do { p++; } while (*p && !isspace(*p)); if (*p) { *p = '\0'; p++; } return GLUE_NAME; } PRIVATE char * GetGlueString(VariableName) char *VariableName; { t_NameRef NameRef = 0; static char buf[30]; if (!glueNameSpace) { return "ERROR: no previous glue expression"; } NameRef = LQU_StringToNameRef(glueNameSpace, VariableName); if (LQU_NameRefIsValid(glueNameSpace, NameRef)) { int value; value = (int) LQU_GetValueFromNameRef(NameRef); (void) sprintf(buf, "%d", value); return buf; } return "ERROR: unknown name"; } PRIVATE void HandleGlue(Line) char *Line; { t_NameRef NameRef = 0; static char *DocumentMatches = 0; if (!glueNameSpace) { MakeGlueNameSpace(); } /* reset the glue parser: */ (void) gettoken((char *) 0); if (gettoken(Line) != GLUE_START) { Error(E_FATAL|E_BUG|E_INTERNAL, "%s: %d: first token on glue line not glue start!", __FILE__, __LINE__ ); } if (gettoken(Line) != GLUE_NAME) { Error(E_WARN, "Mistake in Glue Expression: must start with a name"); return; } /* add the name to the namespace if not already there */ NameRef = LQU_StringToNameRef(glueNameSpace, theTokenString); if (!LQU_NameRefIsValid(glueNameSpace, NameRef)) { Error(E_WARN|E_MULTILINE, "Unknown glue variable %s", theTokenString ); Error(E_WARN|E_MULTILINE|E_LASTLINE, "variables allowed: \"DocumentMatches\"" ); return; } else { /* this should really be an anonymous variable */ LQU_SetNameVariable(NameRef, &DocumentMatches); /* TODO FIXME Hack! */ } if (gettoken(Line) != GLUE_ASSIGN) { Error(E_WARN, "Mistake in Glue Expression: need \"=\" after name"); return; } /* get the value */ switch (gettoken(Line)) { case GLUE_STRING: case GLUE_NUMBER: /* treat them the same...! */ if (LQU_cknatstr(theTokenString)) { (void) LQU_SetNameValue(NameRef, (char *) atoi(theTokenString)); } else { Error(E_WARN|E_MULTILINE, "Mistake in Glue Expression:"); Error(E_WARN|E_MULTILINE|E_LASTLINE, "Value for %s must be numeric", "DocumentMatches" ); return; (void) LQU_SetNameValue(NameRef, 0); } break; default: Error(E_WARN|E_MULTILINE, "Mistake in Glue Expression %s", Line ); Error(E_WARN|E_MULTILINE|E_LASTLINE, "Assignment (=) must be followed by a number or \"string\"" ); return; } /* set the value */ if (gettoken(Line) != GLUE_STOP) { Error(E_WARN, "Mistake in Glue Expression: expected %c to end glue", GLUE_STOP ); return; } } PRIVATE long fReadMatchFile(FileWithMatches, MatchFileName) FILE *FileWithMatches; char *MatchFileName; { long MatchCount = 0; char *Line; int NumberOfWords = 1; unsigned long BlockInFile = 0; unsigned char *FileName = "no filename available"; unsigned long WordInBlock = 0; t_FID FID; if (!FileWithMatches) { Error(E_FATAL, "match-list file (from -f -) has NULL file!"); } while (LQU_fReadLine( FileWithMatches, &Line, LQUF_IGNBLANKS|LQUF_IGNSPACES|LQUF_IGNHASH|LQUF_ESCAPEOK ) >= 0) { register char *p; p = Line; /* ASSERT: There are no leading or trailing spaces on the line */ if (!*p || *p == '#') { continue; /* blank line */ } if (*p == GLUE_START) { /* we have a line like * { MatchCount = 4 } */ HandleGlue(Line); continue; } if (MatchesHaveWordCount) { if (!LQT_ISDIGIT(db, *p)) { Error(E_WARN, "%s: Bad match format (expected digit) in %s", MatchFileName, Line ); return MatchCount; } NumberOfWords = 0; while (LQT_ISDIGIT(db, *p)) { NumberOfWords *= 10; NumberOfWords += *p++ - '0'; } while (isspace(*p)) { p++; } } /* block in file */ BlockInFile = 0L; while (LQT_ISDIGIT(db, *p)) { BlockInFile *= 10; BlockInFile += *p - '0'; p++; } while (isspace(*p)) { p++; } /* Word In Block */ WordInBlock = 0L; while (LQT_ISDIGIT(db, *p)) { WordInBlock *= 10; WordInBlock += *p - '0'; p++; } while (isspace(*p)) { p++; } /* file identifier (FID) */ FID = 0L; while (LQT_ISDIGIT(db, *p)) { FID *= 10; FID += *p - '0'; p++; } while (isspace(*p)) { p++; } /* filename */ if (!FID && !*p) { Error(E_WARN, "%s: bad match format, neither FID nor filename", MatchFileName ); continue; } FileName = p; /* file name, already null-terminated */ if (!*p || !FID) { static char *theName = 0; static t_FID theFID = 0; /* invalid FID */ t_FileInfo *FileInfo; if (!FID) { /* filename and no FID */ if (theName && STREQ(p, theName)) { FID = theFID; } else { FID = LQT_NameToFID(db, FileName); } } else { /* p is null, FID and no filename */ if (FID == theFID) { /* same as last time round, * so no need to look it up again */ FileName = theName; } else { if (theName) { efree(theName); } FileInfo = LQT_FIDToFileInfo(db, FID); theFID = FID; FileName = emalloc( FileInfo->Name, (unsigned) strlen(FileInfo->Name) + 1 ); theName = FileName; (void) strcpy(theName, FileInfo->Name); LQT_DestroyFileInfo(db, FileInfo); } } } ++MatchCount; (void) ShowOneMatch( MatchCount, NumberOfWords, BlockInFile, WordInBlock, FileName, FID ); } if (postMatchFileFormat) { (void) ShowOneMatch( MatchCount, NumberOfWords, BlockInFile, WordInBlock, FileName, (t_FID) 0 ); } return MatchCount; } PRIVATE int ShowOneMatch( MatchNumber, WordCount, BlockInFile, WordInBlock, DocumentName, FID ) long MatchNumber; int WordCount; unsigned long BlockInFile; unsigned long WordInBlock; char *DocumentName; t_FID FID; { static char *Buffer = 0; static char *LastDocumentName = 0; static char *LastFileName = 0; static unsigned long LastBlockInFile = (unsigned long) -1L; static int AmountRead; static int fd = -1; register char *p; register char *q; int InSpace = 0; t_OffsetPair *OffsetPair = 0; char *FirstStart = 0; static char *TextBefore = 0; static char *MatchedText = 0; static char *TextAfter = 0; char *FileName = DocumentName; /** see if the file name has changed **/ if (LastDocumentName == (char *) 0 || ( !STREQ(LastDocumentName, DocumentName) && FID != (t_FID) 0 ) ) { if (LastDocumentName != (char *) 0) { if (fd >= 0) { (void) close(fd); } efree(LastDocumentName); } fd = (-1); LastDocumentName = emalloc("LastDocmnt", strlen(DocumentName) + 1); (void) strcpy(LastDocumentName, DocumentName); LastBlockInFile = (unsigned long) -1L; if (LastFileName) { (void) efree(LastFileName); } FileName = LQT_FindFile(db, DocumentName); if (!FileName) { if ((fd = LQT_UnpackAndOpen(db, DocumentName)) < 0) { Error(E_WARN|E_SYS, "can't find file \"%s\"", DocumentName); return -1; } else { FileName = DocumentName; } } LastFileName = emalloc("LastFileName", strlen(FileName) + 1); (void) strcpy(LastFileName, FileName); } /** if necessary, open the file and read the text **/ if (LazyEvaluation.NeedText) { static unsigned int BufLen; if (fd < 0) { if ((fd = LQT_UnpackAndOpen(db, FileName)) < 0) { return -1; } } if (LastBlockInFile != BlockInFile) { if (LQU_Elseek(E_WARN, DocumentName, FileName, fd, BlockInFile ? (long) ((BlockInFile - 1) * LQT_FileBlockSize(db)) : 0L, SEEK_SET /* = 0 */ ) < 0) { /* LQU_Elseek() printed an error already */ return -1; } /** Ensure that we have a line buffer, if we will need one **/ if (Buffer == (char *) 0) { /* TODO: use the real width from the variable formats */ BufLen = (LeftWidth + RightWidth) * 5; /* Make enough space for a block before and a block after: */ if (BufLen < LQT_FileBlockSize(db) * 3) { BufLen = LQT_FileBlockSize(db) * 3; } Buffer = emalloc("lqkwic line buffer", BufLen); } if ((AmountRead = read(fd, Buffer, BufLen)) < db->MinWordLength) { Error(E_SYS|E_WARN, "%s: read(%d,0x%x,%d) returned %d", DocumentName, fd, Buffer, BufLen, AmountRead ); (void) close(fd); return -1; } LastBlockInFile = BlockInFile; } /** Find the required word */ OffsetPair = LQT_FindMatchEnds( db, Buffer, AmountRead, (BlockInFile == 0) ? Buffer : &Buffer[LQT_FileBlockSize(db)], BlockInFile, /* so LQT_FileBlockSize knows it can go backwards? */ WordInBlock, WordCount ); if (!OffsetPair) { Error(E_WARN, "Match %d (block %ld, word %d, in %s) not found", LineNumber, BlockInFile, WordInBlock, DocumentName ); return 0; /* but carry on */ } /* Find context before the keyword */ if (!TextBefore) { TextBefore = emalloc("lqkwic match.firstbit", LeftWidth + 1); } q = &TextBefore[LeftWidth]; *q = '\0'; InSpace = 0; { int WithinTag = 0; int TagMarkInserted = 0; for (p = OffsetPair->Start - 1; p >= Buffer; --p) { char *Entity; if (q == TextBefore) { break; } if (SuppressTags) { if (WithinTag) { if (*p == '<') { WithinTag = 0; } continue; } else if (*p == '>') { if (!TagMarkInserted) { *--q = TagMark; TagMarkInserted = 1; } WithinTag = 1; continue; } else if ((Entity = EndsEntity(p, Buffer)) != 0) { char *Value = EntityValue(Entity); /* Insert the entity value backwards into the text, * so that if it doesn't all fit, we get the * right-hand end, as the string we are building * will be truncated on the left: */ if (Value && *Value) { char *z = Value; /* find the end: */ while (*z) z++; while (z > Value && q > TextBefore) { *--q = *--z; } /* skip to the start of the entity: */ while (*p != '&') { p--; } (void) efree(Entity); TagMarkInserted = 0; continue; } else { (void) efree(Entity); TagMarkInserted = 0; } } else if (!isspace(*p)) { TagMarkInserted = 0; } } else if (ConvertSGMLTagsToEntities) { if (*p == '<' || *p == '&') { char *Value; char *z; if (*p == '<') { Value = z = "<"; } else { Value = z = "&"; } while (*z) z++; while (z > Value && q > TextBefore) { *--q = *--z; } continue; } } if (isspace(*p)) { if (!InSpace) { *--q = ' '; } InSpace = 1; } else { InSpace = 0; *--q = *p; } } } FirstStart = q; /* now build up the rest of the buffer */ if (!MatchedText) { MatchedText = emalloc("lqkwic MatchedText", RightWidth + 1); } q = MatchedText; *q = '\0'; if (!TextAfter) { TextAfter = emalloc("lqkwic TextAfter", RightWidth + 1); } *TextAfter = '\0'; InSpace = 0; { int WithinTag = 0; int TagMarkInserted = 0; int Length = 0; for (p = OffsetPair->Start; ; p++) { char *Entity; if (++Length > RightWidth) break; if (p > OffsetPair->End && q - TextAfter >= RightWidth) { *q = '\0'; break; } else if (p == OffsetPair->End) { *q = '\0'; q = TextAfter; } if (SuppressTags) { if (WithinTag) { if (*p == '>') { WithinTag = 0; } continue; } else if (*p == '<') { if (!TagMarkInserted) { *q++ = TagMark; *q = '\0'; TagMarkInserted = 1; } WithinTag = 1; continue; } else if ((Entity = StartsEntity(p)) != 0) { char *Value = EntityValue(Entity); /* Insert the entity value into the text */ if (Value && *Value) { while (q - TextAfter < RightWidth && *Value) { *q++ = *Value++; } *q = '\0'; /* skip to the end of the entity: */ while (*p && *p != ';') { p++; } TagMarkInserted = 0; (void) efree(Entity); InSpace = 0; continue; } else { TagMarkInserted = 0; (void) efree(Entity); InSpace = 0; /* but no continue; */ } } else if (!isspace(*p)) { TagMarkInserted = 0; } } else if (ConvertSGMLTagsToEntities) { if (*p == '<' || *p == '&') { char *z; if (*p == '<') { z = "<"; } else { z = "&"; } while (*z && q - TextAfter < RightWidth) { *q++ = *z++; } *q = '\0'; continue; } } if (isspace(*p)) { if (!InSpace) { *q++ = ' '; *q = '\0'; } InSpace = 1; } else { *q++ = *p; *q = '\0'; InSpace = 0; } *q = '\0'; } } *q = '\0'; } { unsigned long StartByte, EndByte; if (OffsetPair) { StartByte = (OffsetPair->Start - Buffer) + (BlockInFile ? (long) ((BlockInFile - 1) * LQT_FileBlockSize(db)) : 0L); EndByte = StartByte + (OffsetPair->End - OffsetPair->Start); } else { /* in this case they won't be used anyway, because we checked * for LazyEvaluation earlier. */ StartByte = EndByte = 0L; } OutputMatch( MatchNumber, FID, LastDocumentName, LastFileName, WordCount, BlockInFile, WordInBlock, FirstStart, MatchedText, TextAfter, StartByte, EndByte ); } if (unbufferedMode) { (void) fflush(stdout); } return 0; } typedef struct s_Entity { char *Name; char *Value; struct s_Entity *Next; } t_Entity; static t_Entity *EntityList = NULL; static void InsertEntity(e) t_Entity *e; { t_Entity **Epp = &EntityList; /* Locate: */ for (Epp = &EntityList; *Epp; Epp = &(*Epp)->Next) { if (STRCMP(e->Name, (*Epp)->Name) >= 0) { break; } } /* Insert: */ e->Next = (*Epp); *Epp = e; LQT_Trace(LQTRACE_DEBUG, "Insert Entity &%s; as \"%s\"\n", e->Name, e->Value ); } PRIVATE char * GetNextString(Source, pp) char *Source; /* e.g. the file name */ char **pp; { char quotes = 0; char *Start; char *String; char *result; if (!pp || !*pp || !**pp) return 0; String = (*pp); /* for error reporting */ while (isspace(**pp)) ++*pp; switch (**pp) { case '\0': return (char *) NULL; case '"': case '\'': quotes = (**pp); ++*pp; break; case '`': quotes = '\''; /* `...' */ ++*pp; break; default: break; } Start = (*pp); /* find the end... */ while (**pp) { if ((quotes && **pp == quotes) || (!quotes && isspace(**pp))) { break; } ++*pp; } if (quotes) { if (**pp != quotes) { Error(E_FATAL, "%s: unmatched quote in %s, expected %c", Source, String, quotes ); } } result = emalloc("lqkwic::GetNextString.result", *pp - Start + 2); (void) strncpy(result, Start, *pp - Start + 1); result[*pp - Start] = '\0'; if (**pp) { ++*pp; } return result; } PRIVATE char * StartsEntity(String) char *String; { register char *p; if (*String != '&') { return 0; } ++String; /* skip over the initial & */ for (p = String; *p; p++) { if (isspace(*p)) return 0; if (*p == ';') { char *Name = emalloc( "lqkwic:SGML Entity Name", (unsigned int)(p - String + 1) ); (void) strncpy(Name, String, p - String); Name[p - String] = '\0'; return Name; } } return 0; } static char * EndsEntity(String, Buffer) char *String; char *Buffer; { register char *p; if (*String != ';') { return 0; } for (p = String; p > Buffer; --p) { if (isspace(*p)) return 0; if (*p == '&') { char *Name; ++p; /* skip over the & */ Name = emalloc("lqkwic:EndsEntity.Name", String - p + 1); (void) strncpy(Name, p, String - p); Name[String - p] = '\0'; return Name; } } return 0; } static t_Entity * NewEntity(String, FileName) char *String; char *FileName; { t_Entity *e; e = (t_Entity *) emalloc("lqkwic:NewEntity.Name", sizeof (t_Entity)); e->Next = 0; e->Name = GetNextString(FileName, &String); e->Value = GetNextString(FileName, &String); return e; } PRIVATE void ReadEntityFile(FileName) char *FileName; { char **Lines = 0; int count; int i; count = LQU_ReadFile( E_FATAL, FileName, "SGML Entity Replacement File", &Lines, /* yes, a (char ***) */ LQUF_IGNBLANKS|LQUF_IGNSPACES|LQUF_IGNHASH|LQUF_ESCAPEOK ); for (i = 0; i < count; i++) { t_Entity *oneEntity = NewEntity(Lines[i], FileName); if (oneEntity) { InsertEntity(oneEntity); } (void) efree(Lines[i]); } (void) efree((char *)Lines); } PRIVATE char * EntityValue(Name) char *Name; { register t_Entity *Ep; int i = 1; /* i.e. non-zero in case loop is never reached */ for (Ep = EntityList; Ep; Ep = Ep->Next) { if ((i = STRCMP(Name, Ep->Name)) >= 0) { break; } } if (i == 0 && Ep) { LQT_Trace(LQTRACE_DEBUG, "SGML Entity &%s; has value \"%s\"\n", Name, Ep->Value ); return Ep->Value; } else { LQT_Trace(LQTRACE_DEBUG, "Entity &%s; has no declared value\n", Name ); return 0; } } static char *FormatStringValue( #ifdef HAVE_PROTO char *Format, char *Value, char *VariableName #endif ); /* the "the" prefix is to work aroung a gcc bug in -Wall and inline... */ static char * NumberToString(theStringFormat, theVariableName, theNumber, theFormat) char *theStringFormat; char *theVariableName; unsigned long theNumber; char *theFormat; { static char NumberBuffer[30]; (void) sprintf(NumberBuffer, theFormat, theNumber); return FormatStringValue(theStringFormat, theVariableName, NumberBuffer); } PRIVATE int StringRefersToVariable(String, Variable) char *String; char *Variable; { char *theName = 0; register char *p; char First = Variable[0]; /* We store the first character of the variable separately for * convenience...: */ First = LQT_TOLOWER(db, First); ++Variable; for (p = String; *p; p++) { if (*p == '$' && p[1] == BRA && ( p[2] == First || (LQT_TOLOWER(db, p[2]) == First) || p[2] == '!' ) ) { register char *q, *np; ++p; /* step over the $ */ ++p; /* step over the { */ while (*p == '!') { ++p; } /* check first character first, hoping to avoid doing * the LQU_DownCase later... */ if (*p == First || (LQT_TOLOWER(db, *p) == First)) { p++; } else { break; } if (!theName) { /* only do this if we need to... * it sometimes calls malloc() and can be slow. * (it returns a pointer to a static buffer, watch out * for recursion!) */ theName = LQU_DownCase(Variable); } for (q = p, np = theName; *q; q++, np++) { if (*q == KET || *q == ':') { return 1; } else if (*q != *np && !(*np == LQT_TOLOWER(db, (*q)))) { break; } } if (*q) { p = q; } else { return 0; } } } return 0; } /* This routine is due to change -- it's crazy to put this * much on the stack for each call! * */ static char * sPrintWithFormat(Format, FileNumber, MatchNumber, MatchWithinFile, FID, DocName, FileName, LastDocName, NumberOfWordsInPhrase, BlockInFile, WordInBlock, TextBefore, MatchedText, TextAfter, StartByte, EndByte ) char *Format; unsigned long FileNumber; long MatchNumber; unsigned long MatchWithinFile; t_FID FID; char *DocName; /* the name stored in the database */ char *FileName; /* the expanded file name */ char *LastDocName; /* the name stored in the database */ int NumberOfWordsInPhrase; unsigned long BlockInFile; unsigned long WordInBlock; char *TextBefore; char *MatchedText; char *TextAfter; unsigned long StartByte, EndByte; { register char *p; char *result; unsigned int allocatedLength = 100; char *Rp; result = emalloc("sPrintWithFormat", allocatedLength); Rp = result; *Rp = '\0'; #define ADDCHAR(ch) \ if (Rp - result > allocatedLength - 2) { \ int Where = Rp - result; \ result = erealloc(result, allocatedLength += 50); \ Rp = &result[Where]; \ } \ *Rp = (ch); \ *++Rp = '\0'; #define ADDSTRING(str) \ { \ register char *_p; \ \ for (_p = str; *_p; _p++) { \ ADDCHAR(*_p); \ } \ } \ #define ADDREVERSESTRING(str, revType) \ { \ int whereWeWere = Rp - result; \ ADDSTRING(str) \ LQU_ReverseString( \ &result[whereWeWere], \ &result[Rp - result], \ revType \ ); \ } \ for (p = Format; *p; p++) { if (*p == '\\') { p++; /* step over the \ */ if (!*p) { Error(E_FATAL|E_MULTILINE, "Expected \\, $, n, r, f, b or e after \\, not end of string, in" ); Error(E_FATAL|E_MULTILINE|E_LASTLINE, "format string [%s]", Format ); } switch (*p) { case '\\': case '$': ADDCHAR(*p); break; case 'n': ADDCHAR('\n'); break; case 'r': ADDCHAR('\r'); break; case 'f': ADDCHAR('\f'); break; case 'b': ADDCHAR('\b'); break; case 'e': ADDCHAR('\037'); /* ESC */ break; default: Error(E_FATAL|E_MULTILINE, "Expected \\, $, n, r, f, b or e after \\, not \"%c\", in", *p ); --p; /* back to the \ */ Error(E_FATAL|E_MULTILINE|E_LASTLINE, "format string [%*.*s>>%c<<%s]", p - Format, p - Format, Format, p[1], &p[2] ); } } else if (*p == '$') { if (p[1] == '$') { ADDCHAR('$'); p++; /* step over the $ */ } else if (p[1] == BRA) { /* interpolate a variable */ register char *End; int reverseTheResult = 0; p++; /* step over the $ */ p++; /* step over the BRA */ while (*p == '!') { ++reverseTheResult; p++; } for (End = p; *End; End++) { if (*End == KET) { char *TheName; char *TheValue; TheName = emalloc("Variable Name", End - p + 1); (void) strncpy(TheName, p, End - p + 1); TheName[End - p] = '\0'; TheValue = GetVariableValue(TheName, FileNumber, MatchNumber, MatchWithinFile, FID, DocName, FileName, LastDocName, NumberOfWordsInPhrase, BlockInFile, WordInBlock, TextBefore, MatchedText, TextAfter, StartByte, EndByte ); if (TheValue) { if (reverseTheResult) { ADDREVERSESTRING(TheValue, reverseTheResult); } else { ADDSTRING(TheValue); } } else { Error(E_FATAL, "Unknown variable ${%s} in format %s", TheName, Format ); } p = End; break; } } if (!*End) { Error(E_FATAL, "Variable name unterminated (no %c) in format \"%s\"", KET, Format ); } } else if (p[1] == '[') { #define SEQUENCES_CANNOT_NEST /* variable name */ register char *End; unsigned int Start; int reverseTheResult = 0; int nestCount = 1; p++; /* step over the $ */ p++; /* step over the [ */ while (*p == '!') { ++reverseTheResult; p++; } /* Keep the start so we can reverse the result, * e.g. for sorting. * * We keep a number because ADDSTRING sometimes * reallocates the buffer, in order to grow it. */ Start = Rp - result; for (End = p; *End; End++) { if (*End == '$' && End[1] == '[') { End += 2; ++nestCount; continue; } if (*End == ']' && --nestCount == 0) { char *TheName; char *TheValue; char *TheFormat; TheName = emalloc("Variable Name", End - p + 1); (void) strncpy(TheName, p, End - p + 1); TheName[End - p] = '\0'; /* Find if there's a format here or not. * The format is indicated by a / as for an * ordinary variable, but unfortunately that's * really hard to tell, because we have to * detect recursion, e.g. in * $[${TextBefore/5.5l}/12r] * or even * $[xxx$[yyy/5r]/5l] * * For now, the rule is that a format can't include * any variables, so we only look back as far as * a / or KET. But I'd actually really like to * do ${TextBefore/${LeftGapWidth}}. * Sigh. */ TheFormat = &TheName[End - p]; while (--TheFormat > TheName) { if (*TheFormat == KET) { TheFormat = ""; break; } else if (*TheFormat == '/') { *TheFormat = '\0'; ++TheFormat; break; } } if (TheFormat == TheName) { TheFormat = ""; } TheValue = sPrintWithFormat(TheName, FileNumber, MatchNumber, MatchWithinFile, FID, DocName, FileName, LastDocName, NumberOfWordsInPhrase, BlockInFile, WordInBlock, TextBefore, MatchedText, TextAfter, StartByte, EndByte ); if (TheValue) { char *z = FormatStringValue( TheFormat, TheName, TheValue ); ADDSTRING(z); efree(TheValue); } efree(TheName); p = End; break; } } if (!*End) { Error(E_FATAL, "format unterminated ($[ with no ]) in format \"%s\"", Format ); } if (reverseTheResult) { LQU_ReverseString(&result[Start], End, reverseTheResult); } } else { /* $ followed by something else */ Error(E_FATAL|E_MULTILINE, "$-sign in a format must be followed by $ or %cvariable%c;", BRA, KET ); Error(E_FATAL|E_MULTILINE, "format contained unexpected character \"%c\" in format:", *p ); Error(E_FATAL|E_MULTILINE, "[%*.*s>>%c<<%s]", p - Format, p - Format, Format, *p, &p[1] ); Error(E_FATAL|E_MULTILINE|E_LASTLINE, "%s -L gives the list of valid variables.", progname ); } } else { /* not a $ sign */ ADDCHAR(*p); } } return result; } PRIVATE void OutputMatch( MatchNumber, FID, DocName, FileName, NumberOfWordsInPhrase, BlockInFile, WordInBlock, TextBefore, MatchedText, TextAfter, StartByte, EndByte ) long MatchNumber; t_FID FID; char *DocName; /* the name stored in the database */ char *FileName; /* the expanded file name */ int NumberOfWordsInPhrase; unsigned long BlockInFile; unsigned long WordInBlock; char *TextBefore; char *MatchedText; char *TextAfter; unsigned long StartByte, EndByte; { /* LastDocName is the last document name that was different * from the current one. * PrevDocName is the document name (DocName) from the * immediately preceding call to this function, * used only so that we can tell when DocName changes. */ static char *LastDocName = 0; static char *PrevDocName = 0; static int FileNumber = 0; static int MatchWithinFile = 0; static NeedDocHeader = 1; static t_FID PrevFID = 0; /* see if the file has changed: */ if (FID != PrevFID) { if (PrevFID && postMatchFileFormat) { char *p; /* TODO: fix this mess so that we call this with * the right values, i.e. the ones from last time round! */ p = sPrintWithFormat(postMatchFileFormat, FileNumber, MatchNumber, MatchWithinFile, PrevFID, PrevDocName, FileName, LastDocName, NumberOfWordsInPhrase, BlockInFile, WordInBlock, TextBefore, MatchedText, TextAfter, StartByte, EndByte ); fputs(p, stdout); efree(p); } if (FID == (t_FID) 0) { return; } PrevFID = FID; ++FileNumber; if (LastDocName) { efree(LastDocName); } LastDocName = PrevDocName; PrevDocName = emalloc("LastDocName", strlen(DocName) + 1); (void) strcpy(PrevDocName, DocName); NeedDocHeader = 1; MatchWithinFile = 1; if (RangeOfFilesToPrint) { if (!LQU_NumberWithinRange(FileNumber, RangeOfFilesToPrint)) { if (LQU_LargerThanRangeTop(FileNumber, RangeOfFilesToPrint)) { exit(0); } return; } } } else { ++MatchWithinFile; } if (RangeOfMatchesToPrint) { if (!LQU_NumberWithinRange(MatchNumber, RangeOfMatchesToPrint)) { if (LQU_LargerThanRangeTop(MatchNumber, RangeOfMatchesToPrint)) { exit(0); } return; } } if (NeedDocHeader && theMatchFileFormat) { char *p = sPrintWithFormat(theMatchFileFormat, FileNumber, MatchNumber, MatchWithinFile, FID, DocName, FileName, LastDocName, NumberOfWordsInPhrase, BlockInFile, WordInBlock, TextBefore, MatchedText, TextAfter, StartByte, EndByte ); fputs(p, stdout); efree(p); NeedDocHeader = 0; } if (theMatchFormat) { char *p = sPrintWithFormat(theMatchFormat, FileNumber, MatchNumber, MatchWithinFile, FID, DocName, FileName, LastDocName, NumberOfWordsInPhrase, BlockInFile, WordInBlock, TextBefore, MatchedText, TextAfter, StartByte, EndByte ); fputs(p, stdout); efree(p); } } #define IsDirectionChar(ch) ((ch) == 'l' || (ch) == 'r' || (ch) == 'c') static char * FormatStringValue(Format, VariableName, Value) char *Format; char *Value; char *VariableName; { static char *result = 0; static unsigned int ResultLength = 0; int MinimumWidth = 0; int MaximumWidth = 0; int LeftPad; int RightPad; int ValueLength; register char *p; char PadDirection = 'l'; char TruncateDirection = 'r'; int CharsToPrint; /* No. of characters to take from the value string */ /* Format syntax: * minwidth [maxwidth] [l|r|c] */ if (Value && *Value) { CharsToPrint = ValueLength = strlen(Value); } else { CharsToPrint = ValueLength = 0; } for (p = Format; isspace(*p); p++) ; if (!*p) { /* no format, or empty format, return the value unchanged: */ return Value ? Value : ""; } else if (*p == '*') { MinimumWidth = 0; p++; } else if (!isdigit(*p)) { Error(E_FATAL, "${%s/%s}: format must start with * or minimum width, not \"%c\"", VariableName, Format, *p ); } else { /* a digit */ do { MinimumWidth *= 10; MinimumWidth += *p++ - '0'; } while (*p && isdigit(*p)); if (IsDirectionChar(*p)) { PadDirection = (*p); p++; } else if (*p && !isspace(*p)) { Error(E_FATAL, "${%s/%s}: unexpected format character %c, use l, c, r or none", VariableName, Format ); } } while (isspace(*p)) { p++; } MaximumWidth = MinimumWidth; LeftPad = RightPad = 0; if (*p == '*') { /* ignored */ } else if (isdigit(*p)) { MaximumWidth = 0; do { MaximumWidth *= 10; MaximumWidth += *p++ - '0'; } while (isdigit(*p)); if (IsDirectionChar(*p)) { TruncateDirection = (*p); p++; } else if (*p && !isspace(*p)) { Error(E_FATAL, "${%s/%s}: unexpected format character %c, use l, c, r or none", VariableName, Format ); } if (CharsToPrint > MaximumWidth) { /* need to truncate the value */ CharsToPrint = MaximumWidth; } } if (MinimumWidth > CharsToPrint) { switch (PadDirection) { case 'l': /* left justify, i.e. pad at the right */ RightPad = MinimumWidth - CharsToPrint; break; case 'c': LeftPad = (MinimumWidth - CharsToPrint + 1) / 2; RightPad = (MinimumWidth - CharsToPrint) / 2; break; case 'r': /* right justify, i.e. pad at the left */ LeftPad = MinimumWidth - CharsToPrint; break; } } if (!result) { ResultLength = LeftPad + CharsToPrint + RightPad + 1; result = emalloc("FormatStringValueResult", ResultLength); } else if (ResultLength < LeftPad + CharsToPrint + RightPad + 1) { ResultLength = LeftPad + CharsToPrint + RightPad + 1; result = erealloc(result, ResultLength); } p = result; while (LeftPad-- > 0) { *p++ = ' '; } /* put out the string */ if (Value && *Value) { register char *q; switch (TruncateDirection) { case 'r': default: /* chop off the right hand end */ for (q = Value; q - Value < CharsToPrint; q++) { *p++ = (*q); } break; case 'l': /* chop off the left hand end */ { register int i = 0; for ( q = &Value[ValueLength - CharsToPrint]; i < CharsToPrint; q++, i++ ) { *p++ = (*q); } } break; case 'c': { register int i = 0; for ( q = &Value[(ValueLength - CharsToPrint)/2]; i < CharsToPrint; q++, i++ ) { *p++ = (*q); } } break; } } while (RightPad-- > 0) { *p++ = ' '; } *p = '\0'; return result; /* nothing left to print */ } /* global variables (!) required for termcap: */ int ospeed = 9600; /* actually don't care about the value really */ int PC = '\0'; #define TI_STRING 01 #define TI_NUMBER 02 #define TI_BOOLEAN 03 /* NOTDONE */ static char *tgetStringPointer; static char *tgetString; static unsigned int tgetStringLength = 0; static int AddToString(ch) int ch; { if (tgetStringPointer - tgetString >= tgetStringLength + 2) { int Position = tgetStringPointer - tgetString; tgetStringLength += 20; tgetString = erealloc(tgetString, tgetStringLength); tgetStringPointer = &tgetString[Position]; } *tgetStringPointer = ch; return *++tgetStringPointer = '\0'; } PRIVATE char * GetTermInfoString(Name, ValueType) char *Name; int ValueType; { static char *TerminalEntry = 0; char *result; extern char *tgetstr(); static char Buffer[1024]; char *p = Buffer; if (!TerminalEntry) { /* initialise */ char *TerminalName = getenv("TERM"); int Status; TerminalEntry = emalloc("Terminal Entry", 1024); /* see termcap(3X) */ if (!TerminalName) { TerminalName = "unknown"; } #ifdef CURSESX setupterm(TerminalEntry, fileno(stdout), &Status); #else Status = tgetent(TerminalEntry, TerminalName); #endif switch (Status) { case 0: #ifdef CURSESX Error(E_FATAL, "Terminal type \"%s\" has no terminfo entry", TerminalName ); #else Error(E_FATAL, "Terminal type \"%s\" has no termcap entry", TerminalName ); #endif case 1: /* OK */ break; case -1: default: Error(E_FATAL|E_SYS, "couldn't access termcap file or terminfo database for \"%s\"", TerminalName ); } } /* now we have a terminal entry */ switch (ValueType) { case TI_STRING: p = &Buffer[0]; result = tgetstr(Name, &p); break; case TI_NUMBER: { int n = tgetnum(Name); (void) sprintf(Buffer, "%d", n); result = &Buffer[0]; p = &result[1]; } break; case TI_BOOLEAN: { int val = tgetflag(Name); (void) sprintf(Buffer, "%d", val); result = &Buffer[0]; p = &result[1]; } default: Error(E_BUG|E_FATAL, "GetTermInfoString(%s, %d) where %d is not a supported type", Name, ValueType, ValueType ); /*NOTREACHED*/ result = ""; } if (!result || (p == Buffer)) { if (LQT_TraceFlagsSet(LQTRACE_VERBOSE|LQTRACE_DEBUG)) { #ifdef CURSESX (void) sprintf(Buffer, "[terminfo capability \"%s\" not found]", Name ); #else (void) sprintf(Buffer, "[field \"%s\" not found in termcap entry]", Name ); #endif } result = Buffer; Buffer[0] = '\0'; } if (!tgetStringLength) { tgetStringLength = strlen(result) * 2 + 10; /** allow for padding **/ tgetString = emalloc("tgetString", tgetStringLength); } tgetStringPointer = tgetString; *tgetString = '\0'; (void) tputs(result, 1, AddToString); return tgetString; } static t_NameSpaceTableEntry NameSpaceTable[] = { /** Database related **/ { "LQTEXTDIR", LQU_NameType_String, }, { "DatabaseDirectory", LQU_NameType_String, }, { "DatabaseTitle", LQU_NameType_String, }, { "DatabaseURL", LQU_NameType_String, }, { "DatabaseAdminEMail", LQU_NameType_String, }, { "DatabaseDataEMail", LQU_NameType_String, }, /** Document related: **/ { "DocName", LQU_NameType_String, }, { "DocTitle", LQU_NameType_String, }, { "FileName", LQU_NameType_String, }, { "FileNumber", LQU_NameType_Integer, }, { "FID", LQU_NameType_Integer, }, /** Match related: **/ { "BlockInFile", LQU_NameType_Long, }, { "WordInBlock", LQU_NameType_Long, }, { "StartByte", LQU_NameType_Integer, }, { "EndByte", LQU_NameType_Integer, }, { "MatchedText", LQU_NameType_String, }, { "TextBefore", LQU_NameType_String, }, { "TextAfter", LQU_NameType_String, }, { "MatchLength", LQU_NameType_Integer, }, { "MatchFileFormat", LQU_NameType_String, }, { "MatchFormat", LQU_NameType_String, }, { "NumberOfWordsInPhrase", LQU_NameType_Integer, }, /** history: **/ { "LastDocName", LQU_NameType_String, }, { "MatchNumber", LQU_NameType_Integer, }, { "MatchWithinFile", LQU_NameType_Integer, }, /** Characters, mostly for convenience **/ { "BackSpace", LQU_NameType_Character, }, { "CR", LQU_NameType_Character, }, { "LF", LQU_NameType_Character, }, { "FF", LQU_NameType_Character, }, { "Escape", LQU_NameType_Character, }, { "NL", LQU_NameType_Character, }, { "Quote", LQU_NameType_Character, }, { "DoubleQuote", LQU_NameType_Character, }, { "Return", LQU_NameType_Character, }, /** program related: **/ { "ProgramName", LQU_NameType_String, &progname, }, { "CommandName", LQU_NameType_String, }, { "Revision", LQU_NameType_String, &Revision }, /** terminate the list: **/ { 0, } }; /* Filters */ #define FILTER_END_OF_STRING -1 #define FILTER_DROP_CHARACTER -2 #define FILTER_RESET -3 PRIVATE int F_SplitAtPunctuation(c) int c; { if (!isascii(c) || !isalnum(c)) { return ' '; } return c; } PRIVATE int F_RemovingLeadingPathComponenets(c) int c; { static int position = 0; static short componentCount = 0; static char seenDot = 0; if (c == FILTER_RESET) { position = 0; componentCount = 0; seenDot = 0; } if (c == '/') { seenDot = 1; ++componentCount; } return componentCount < TruncateAtPath ? FILTER_DROP_CHARACTER : c; } PRIVATE int F_DeleteAfterDot(c) int c; { static char seenDot = 0; if (c == FILTER_RESET) { seenDot = 0; } if (c == '.') { seenDot = 1; } return seenDot ? FILTER_DROP_CHARACTER : c; } PRIVATE int F_DeletePunctuation(c) int c; { if (isascii(c) && ispunct(c)) { return FILTER_DROP_CHARACTER; } return c; } PRIVATE int F_ConvertToLower(c) int c; { return LQT_TOLOWER(db, c); } PRIVATE int F_ConvertToUpper(c) int c; { return LQT_TOUPPER(db, c); } PRIVATE int F_RemoveSpaces(c) int c; { if (isascii(c) && isspace(c)) { return FILTER_DROP_CHARACTER; } return c; } static t_NameSpaceTableEntry FilterTable[] = { { "DeletePunctuation", LQU_NameType_Character, F_DeletePunctuation, LQU_NS_FunctionNoArguments}, { "SplitAtPunctuation", LQU_NameType_Character, F_SplitAtPunctuation, LQU_NS_FunctionNoArguments}, { "DeleteAfterDot", LQU_NameType_Character, F_DeleteAfterDot, LQU_NS_FunctionNoArguments}, { "ToLowerCase", LQU_NameType_Character, F_ConvertToLower, LQU_NS_FunctionNoArguments }, { "ToUpperCase", LQU_NameType_Character, F_ConvertToUpper, LQU_NS_FunctionNoArguments }, { "RemoveSpaces", LQU_NameType_Character, F_RemoveSpaces, LQU_NS_FunctionNoArguments }, { "DeleteDirectories", LQU_NameType_Character, F_RemovingLeadingPathComponenets, LQU_NS_FunctionNoArguments }, /** terminate the list: **/ { 0, } }; static t_NameSpace *filterNameSpace = 0; PRIVATE void MakeFilterNameSpace() { filterNameSpace = LQU_NameSpaceTableToNameSpace( "Filter Table", FilterTable ); if (!filterNameSpace) { Error(E_FATAL|E_INTERNAL, "couldn't create name space from filter table" ); } } PRIVATE void ListFilterNames(stream) FILE *stream; { t_NameRef NameRef; if (stream == stderr) { Error(E_FATAL|E_MULTILINE, "Valid filters are as follows:"); } else { fprintf(stream, "Valid filters are as follows:\n"); } if (!filterNameSpace) { MakeFilterNameSpace(); } for ( NameRef = LQU_FirstNameRef(filterNameSpace); LQU_NameRefIsValid(filterNameSpace, NameRef); NameRef = LQU_NextNameRef(filterNameSpace, NameRef) ) { if (stream == stderr) { Error(E_FATAL|E_MULTILINE, " :%s", LQU_GetNameFromNameRef(NameRef) ); } else { fprintf(stream, " :%s\n", LQU_GetNameFromNameRef(NameRef) ); } } } PRIVATE char * applyOneFilter(theFilterName, theString) char *theFilterName; char *theString; { t_NameRef NameRef; int (* filter)(); char *p, *q; static char *result; static int allocatedLength; register int c; if (!filterNameSpace) { MakeFilterNameSpace(); } if (!result) { allocatedLength = 100; result = emalloc("applyFilter buffer", allocatedLength); } /* get the filter */ NameRef = LQU_StringToNameRef(filterNameSpace, theFilterName); if (!LQU_NameRefIsValid(filterNameSpace, NameRef)) { ListFilterNames(stderr); Error(E_FATAL|E_MULTILINE|E_LASTLINE, "Unknown filter \":%s\".", theFilterName ); } filter = (int (*)()) LQU_GetVariableFromNameRef(NameRef); /* reset the filter */ (void) (* filter)(FILTER_RESET); /* apply the filter */ for (p = theString, q = result; *p; p++) { c = (* filter)(*p); if (c == FILTER_END_OF_STRING) { break; } else if (c != FILTER_DROP_CHARACTER) { if (q - result >= allocatedLength - 1) { int whereWeWere = q - result; if (allocatedLength < 500) { allocatedLength += 100; } else if (allocatedLength < 10000) { allocatedLength += 1000; } else { /* also make it a whole number in base 2: */ allocatedLength += 10000; allocatedLength |= 0xFFF; allocatedLength++; } result = erealloc(result, allocatedLength); q = &result[whereWeWere]; } *q++ = c; } } *q = '\0'; return result; } PRIVATE char * applyFilters(theFilters, theString) char *theFilters; char *theString; { char *nameStart, *nameEnd; char filterName[200]; static int allocatedLength; static char *result; if (!result) { allocatedLength = strlen(theString) + 1; if (allocatedLength < 100) { allocatedLength = 100; } } result = emalloc("apply Filters...", allocatedLength); (void) strcpy(result, theString); for (nameStart = theFilters; *nameStart; nameStart = &nameEnd[1]) { for (nameEnd = &nameStart[1]; *nameEnd; nameEnd++) { if (*nameEnd == ':') { break; } } if (nameEnd > nameStart) { char *tmp; int n; int reverseType = 0; /* look for leading ! signs for reversing the result */ while (*nameStart == '!') { ++nameStart; ++reverseType; } (void) strncpy(filterName, nameStart, nameEnd - nameStart); filterName[nameEnd - nameStart] = '\0'; tmp = applyOneFilter(filterName, result); n = strlen(tmp); if (n > allocatedLength - 1) { allocatedLength = n + 5; /* don't bother with realloc(), avoid the copying: */ efree(result); result = emalloc("new filter result", allocatedLength); } (void) strcpy(result, tmp); if (reverseType) { LQU_ReverseString(result, &result[strlen(result)], reverseType); } } if (!*nameEnd) { break; } } return result; } PRIVATE char * GetVariableValue(VariableName, FileNumber, MatchNumber, MatchWithinFile, FID, DocName, FileName, LastDocName, NumberOfWordsInPhrase, BlockInFile, WordInBlock, TextBefore, MatchedText, TextAfter, StartByte, EndByte ) char *VariableName; unsigned long FileNumber; long MatchNumber; unsigned long MatchWithinFile; t_FID FID; char *DocName; /* the name stored in the database */ char *FileName; /* the expanded file name */ char *LastDocName; /* the previous DocName */ int NumberOfWordsInPhrase; unsigned long BlockInFile; unsigned long WordInBlock; char *TextBefore; char *MatchedText; char *TextAfter; unsigned long StartByte, EndByte; { char *DownCasedName; char *Format = ""; register char *p; char *filter = ""; char *result = 0; if (!VariableName) { Error(E_FATAL|E_BUG, "GetVariableValue:%s:%d: VariableName is a null pointer", __FILE__, __LINE__, VariableName ); } { int seenFilter = 0; for (p = VariableName; *p; p++) { if (*p == '/') { *p = '\0'; Format = ++p; } else if (*p == ':' && !seenFilter) { *p = '\0'; filter = ++p; seenFilter = 1; } } } if (!*VariableName) { Error(E_FATAL|E_BUG, "GetVariableValue:%s:%d: VariableName is an empty string", __FILE__, __LINE__, VariableName ); } DownCasedName = LQU_DownCase(VariableName); /* LQU_DownCase uses a static buffer, no need to free the result */ /* the variables are sorted in approximate expected order of frequency of * use within each case: */ switch (DownCasedName[0]) { case 'b': if (STREQ(DownCasedName, "blockinfile")) { result = NumberToString(Format, VariableName, BlockInFile, "%lu"); } else if ( STREQ(DownCasedName, "backspace") || STREQ(DownCasedName, "bs") ) { result = FormatStringValue(Format, VariableName, "\b"); } break; case 'c': if (STREQ(DownCasedName, "cr") || /* see also return */ STREQ(DownCasedName, "carriagereturn")) { result = FormatStringValue(Format, VariableName, "\r"); } break; case 'd': if (STREQ(DownCasedName, "docname")) { result = FormatStringValue(Format, VariableName, DocName); } else if (STREQ(DownCasedName, "doctitle")) { char *TheTitle = LQT_FIDToDocumentTitle(db, FID, DocName); result = FormatStringValue(Format, VariableName, TheTitle); } else if (STREQ(DownCasedName, "databasedirectory")) { result = FormatStringValue( Format, VariableName, db->DatabaseDirectory ); } else if (STREQ(DownCasedName, "databasetitle")) { result = FormatStringValue( Format, VariableName, db->Options.title.Value ); } else if (STREQ(DownCasedName, "databaseurl")) { result = FormatStringValue( Format, VariableName, db->Options.cgi_url.Value ); } else if (STREQ(DownCasedName, "databaseadminemail")) { result = FormatStringValue( Format, VariableName, db->Options.administrator_email.Value ); } else if (STREQ(DownCasedName, "databasedataemail")) { result = FormatStringValue( Format, VariableName, db->Options.data_owner_email.Value ); } break; case 'e': if (STREQ(DownCasedName, "endbyte")) { result = NumberToString(Format, VariableName, EndByte, "%lu"); } else if ( STREQ(DownCasedName, "escape") || STREQ(DownCasedName, "esc") ) { result = FormatStringValue(Format, VariableName, "\027"); } break; case 'f': if (STREQ(DownCasedName, "filename")) { result = FormatStringValue(Format, VariableName, FileName); } else if (STREQ(DownCasedName, "filenumber")) { result = NumberToString(Format, VariableName, FileNumber, "%lu"); } else if (STREQ(DownCasedName, "fid")) { result = NumberToString(Format, VariableName, FID, "%lu"); } else if ( STREQ(DownCasedName, "ff") || STREQ(DownCasedName, "formfeed") ) { result = FormatStringValue(Format, VariableName, "\f"); } break; case 'g': if (STREQ(DownCasedName, "gap")) { static unsigned char *Gap = 0; if (!Gap) { register unsigned char *p2; Gap = emalloc("Gap String", GapWidth + 1); for (p2 = Gap; p2 - Gap < GapWidth; p2++) { *p2 = ' '; } *p2 = '\0'; } result = FormatStringValue(Format, "Gap", Gap); } else if (DownCasedName[1] == '.') { result = FormatStringValue( Format, VariableName, GetGlueString(&VariableName[2]) ); } break; case 'l': if (STREQ(DownCasedName, "leftpad")) { static char *LeftPad = 0; int Length; register char *p2; if (LeftPad) { (void) efree(LeftPad); } Length = LeftWidth - strlen(TextBefore); if (Length <= 0) result = ""; LeftPad = emalloc("LeftPad buffer", Length + 1); for (p2 = LeftPad; *p2; p2++) { *p2 = ' '; } *p2 = '\0'; result = FormatStringValue(Format, VariableName, LeftPad); } else if (STREQ(DownCasedName, "lastdocname")) { result = FormatStringValue(Format, VariableName, LastDocName); } else if ( STREQ(DownCasedName, "lf") || STREQ(DownCasedName, "linefeed") ) { result = FormatStringValue(Format, "\n", theMatchFormat); } else if (STREQ(DownCasedName, "lqtextdir")) { result = FormatStringValue( Format, VariableName, db->DatabaseDirectory ); } break; case 'm': if (STREQ(DownCasedName, "matchedtext")) { result = FormatStringValue(Format, VariableName, MatchedText); } else if (STREQ(DownCasedName, "matchnumber")) { result = NumberToString(Format, VariableName, MatchNumber, "%lu"); } else if (STREQ(DownCasedName, "matchwithinfile")) { result = NumberToString( Format, VariableName, MatchWithinFile, "%lu" ); } else if (STREQ(DownCasedName, "matchlength")) { result = NumberToString( Format, VariableName, EndByte - StartByte, "%lu" ); } else if (STREQ(DownCasedName, "matchfileformat")) { result = FormatStringValue(Format, VariableName,theMatchFileFormat); } else if (STREQ(DownCasedName, "matchformat")) { result = FormatStringValue(Format, VariableName, theMatchFormat); } break; case 'n': if (STREQ(DownCasedName, "numberofwordsinphrase")) { result = NumberToString(Format, VariableName, NumberOfWordsInPhrase, "%d" ); } else if ( STREQ(DownCasedName, "nl") || STREQ(DownCasedName, "newline") ) { result = FormatStringValue(Format, VariableName, "\n"); } break; case 'p': if (STREQ(DownCasedName, "postfilematch")) { /* see also cr */ result = FormatStringValue(Format, VariableName, "postFileMatch"); } break; case 'q': if (STREQ(DownCasedName, "quote")) { /* see also cr */ result = FormatStringValue(Format, VariableName, "\""); } break; case 'r': if (STREQ(DownCasedName, "return")) { /* see also cr */ result = FormatStringValue(Format, VariableName, "\r"); } else if (STREQ(DownCasedName, "revision")) { /* see also cr */ result = FormatStringValue(Format, VariableName, Revision); } break; case 's': if (STREQ(DownCasedName, "startbyte")) { result = NumberToString(Format, VariableName, StartByte, "%lu"); } break; case 't': if (STREQ(DownCasedName, "textbefore")) { result = FormatStringValue(Format, VariableName, TextBefore); } else if (STREQ(DownCasedName, "textafter")) { result = FormatStringValue(Format, VariableName, TextAfter); } else if (DownCasedName[1] == '.') { /* t.terminfo-name */ result = FormatStringValue( Format, VariableName, GetTermInfoString(&VariableName[2], TI_STRING) ); } else if (DownCasedName[1] == '#') { /* t#terminfo-name-number */ result = FormatStringValue( Format, VariableName, GetTermInfoString(&VariableName[2], TI_NUMBER) ); } else if (DownCasedName[1] == '?') { /* t?terminfo-boolean */ result = FormatStringValue( Format, VariableName, GetTermInfoString(&VariableName[2], TI_BOOLEAN) ); } break; case 'v': if (STREQ(DownCasedName, "variablename" )) { result = FormatStringValue(Format, VariableName, VariableName); } break; case 'w': if (STREQ(DownCasedName, "wordinblock")) { result = NumberToString(Format, VariableName, WordInBlock, "%lu"); } break; } if (!result) { LQT_Trace(LQTRACE_DEBUG, "GetVariableValue(%s) -> \"%s\" drew a blank, sorry.", VariableName, DownCasedName ); } if (filter && *filter) { return applyFilters(filter, result); } else { return result; } } static char *VariableHelp[] = { "Variables recognised with -s and -S options:", "", "All variable names are converted to lower case before being used,", "so you can type them how you want. Exception: variables with names", "starting with \"t.\" are retained as they were given.", "", "[1] ASCII Character names:", "BS, CR FF, LF, NL, BackSpace, ESC, Escape, FormFeed, LineFeed, NewLine,", "Return, Quote (\"), CarriageReturn -- each replaced by the", "corresponding ASCII character.", "", "[2] Global Database Information", "LQTEXTDIR, DatabaseDirectory -- the full path to the database", "DatabaseTitle -- the title of the database", "DatabaseURL -- a URL that gives Word Wide Web access to the database", "DatabaseAdminEMail -- email address of the dtabase administrator", "DatabaseDataEMail -- email address of whoever owns/publishes the data", "", "[3] Files and Documents", "DocName -- the name of the current document, as stored in the database", "FileName -- the apsolute path corresponding to ${DocName}", "DocTitle -- the title of the document", "FID -- the File Identifier Number of the document", "FileNumber -- starts at 1, increases for each new document in the output", "LastDocName -- the last value of DocName that was different; the format", " given with -S (${MatchFileFormat}) is used each time DocName changes.", "", "[4] Match-Specific Variables", "BlockInFile, WordInBlock -- these determine the location of the match", "NumberOfWordsInPhrase -- the length in words of the phrase matched", "TextBefore -- the text in the document immediately before the match", "MatchedText -- the document text that exactly matches the phrase", "TextAfter -- the text in the document immediately after the match", "MatchNumber -- starts at 1 and increases for each match; the format", " given with -s (${MatchFormat}) is used to print each match.", "MatchWithinFile -- like MatchNumber but reset for each new document", "StartByte -- the byte offset in the file at which the match begins", "EndByte -- the byte offset in the file at which the match ends", "Matchlength -- length in bytes of ${MatchedText} (EndByte - StartByte)", "", "[5] Terminal-Specific Variables", "t.so, t.se -- start and end stand-out mode (usually reverse video)", "t.xx -- xx is looked up in the termcap/terminfo database for your", "terminal; xx can be any string. If this doesn't do what you want,", "using `tput xx` is often better (/usr/5bin/tput xx for SunOS 4.x).", "", "[6] Output-Formatting Variables", "Gap -- a string of spaces, corresponding to the -g command-line argument.", "LeftPad -- enough spaces to pad ${TextBefore} to the width given in -l n", "RightPad -- enough spaces to pad ${TextAfter} to the width given in -r n", "MatchFileFormat - the argument given to -S, or the default value", "Matchformat - the argument given to -s, or the default value", "", "Use these variables by including (for example) ${TextBefore} in the", "argument to the -s or -S command-line options.", "", "", "You can give a width specification, e.g. ${TextBefore/9l 12r};", "this means that the text will be padded with spaces on the right if", "it is shorter then 9 characters, making it left-justified; if it is", "longer than 12 characters, it will be truncated from the left, so that", "the right-hand end is retained.", "Valid characters are l, r and c; a width of * means to use the actual", "length of the string.", "", "You can apply format specifications to a sequence, not just to a", "variable, using $[stuff to be affected/5.3r], for example.", #ifdef SEQUENCES_CANNOT_NEST "Such sequences do not nest, however, at the moment.", #endif "", "You can use a leading ! sign to reverse the contents of a", "variable, e.g. ${!TextBefore} will turn \"Simon\" into \"nomiS\".\"", "If you use !!, the string will be reversed, and then each individual", "word will be reveresed, and if you use !!! the whole thing will again", "be reversed; !!!! will reverse the words in that, giving the original.", "Example: given $[barefoot boy in blue], you will get:", " with ! eulb ni yob tooferab", " with !! blue in boy barefoot", " with !!! tooferab yob ni eulb", " with !!!! barefoot boy in blue", " This is used by lqsort, for example, to sort matched words together.", "", "Use \\\\, \\$, \\n, \\r, \\f, \\b or \\e to insert the corresponding", "character (\\e is Escape, ASCII ESC); $$ also inserts a single $ sign.", "", "Filters:", "You can apply any of a number of filters to a variable by appending", "a colon and the name of the filter, for example ${DocTitle:ToLowerCase}.", (char *) 0 }; PRIVATE void ListVariableValues() { puts(""); puts("Current settings of variables with default values:"); printf(" ${MatchFileFormat}=\"%s\"\n", theMatchFileFormat); printf(" ${PostMatchFileFormat}=\"%s\"\n", postMatchFileFormat ? postMatchFileFormat : "[unset]" ); printf(" ${MatchFormat}=\"%s\"\n", theMatchFormat); printf(" ${Gap}=\"");; { int i; for (i = 0; i < GapWidth; i++) { putchar(' '); } } printf("\"\n"); printf(" ${Revision}=\"%s\"\n", Revision); } PRIVATE void ListVariables() { char **p; fflush(stderr); /* in case there were error messages */ for (p = VariableHelp; *p; p++) { printf(" %s\n", *p); } ListVariableValues(); ListFilterNames(stdout); fflush(stdout); /* so it doesn't get mingled with more error messages */ }