/* defaults.c -- Copyright 1989,1993,1994,1996 Liam R. E. Quin. * All Rights Reserved. * This code is NOT in the public domain. * See the file COPYRIGHT for full details. * * $Id: defaults.c,v 1.43 97/01/20 21:27:04 lee Exp $ * * * This file deals with database configuration and preferences. * * It is likely to change considerably in the near future... * */ #include "error.h" #include "globals.h" /* defines and declarations for database filenames */ #include #include #include #ifdef HAVE_FCNTL_H # ifdef HAVE_SYSV_FCNTL_H # include # endif # include #endif #ifdef HAVE_STRING_H # include #else # include #endif #ifdef HAVE_STDLIB_H # include #else # include #endif #ifdef HAVE_UNISTD # include #endif #include "emalloc.h" #include "fileinfo.h" #include "wordinfo.h" #include "phrase.h" #include "lqutil.h" #include "liblqtext.h" #include "lqconfig.h" #include "lqtrace.h" #include "chartype.h" /* Warning: we can't use LQT_ISLOWER and friends until the database * is fully opened... */ #ifndef tolower extern int tolower( # ifdef HAVE_PROTO int theChar # endif ); #endif #define U32(x) (unsigned int)(x) /* System and Library calls used in this function: * */ PRIVATE int NextChar( #ifdef HAVE_PROTO FILE *fd, char *Name, int Map #endif ); PRIVATE char *FromWhereString( #ifdef HAVE_PROTO t_FromWhere Whence #endif ); /** **/ PRIVATE void LQTp_PrintOneTraceFlag(Name, Value, isSet) char *Name; unsigned int Value; int isSet; { fprintf(stderr, "\t%08o %s%s\n", Value, Name, isSet ? " (this flag is set)" : "" ); } /* * LQT_InitFromArgv * Database/Defaults * *

This function is called to Initialise the lq-text libraries. * It sets the global variable progname from argv[0], * but does not remove any leading directories; * if you want just the command name to * appear in error messages and other output, you should set progname * in main() before calling LQT_InitFromArgv.

*

After setting progname, LQT_InitFromArgv handles any lq-text * command-line options. Currently, each option is turned into either * -z if it does not take an argument, or -Z if it take an argument. * As a result, you should ignore -z and -Z options if they appear, * together with the argument to -Z, and you should not give your * program a -z or -Z option. This behaviour will change completely * in a future release of lq-text, when improved command-line * argument handling is introduced.

*

The command line options currently understood include: * *

  • -d dir, to specify a database directory
  • *
  • -m p|h|a, to specify whether to match phrases precisely, * heuristically, or approximately;
  • *
  • -t flags, to turn on tracing; the given flags should be * a string of debugging flag names separated by the vertical bar (|). * An example would be -t Trace|Debug, but you will usually need to * quote the argument to protect it from the shell. * The value List will print a list of available values.
  • * * * Must be called before any other liblqtext functions. * * A pointer to an object used to represent options; this object should * be passed to LQT_OpenDatabase(). * * LQT_OpenDatabase *
    */ API t_lqdbOptions * LQT_InitFromArgv(argc, argv) int argc; char **argv; { char *p; t_lqdbOptions *Result; Result = (t_lqdbOptions *) emalloc("options", U32(sizeof(t_lqdbOptions))); /* This should really be a Name Space!!! */ LQCF_SetOptionDefault(&Result->directory, (char *) 0, "directory"); LQCF_SetOptionDefault(&Result->stoplist, (char *) 0, "stop list"); LQCF_SetOptionDefault( &Result->filesearchpath, DFLTDOCPATH, "file search path" ); LQCF_SetOptionDefault( &Result->phrasematchlevel, (int) PCM_HalfCase, "phrase match level" ); LQCF_SetOptionDefault( &Result->title, (char *) 0, "title" ); LQCF_SetOptionDefault( &Result->administrator_email, (char *) 0, "email address for adminstrator" ); LQCF_SetOptionDefault( &Result->data_owner_email, (char *) 0, "email address for data owner or publisher" ); LQCF_SetOptionDefault( &Result->cgi_url, (char *) 0, "Word Wide Web access to this database" ); LQCF_SetOptionDefault( &Result->locale, (char *) 0, "locale" ); LQCF_SetOptionDefault( &Result->wordflags, LQC_DEFAULT_WORD_FLAGS, /* globals.h */ "word flags" ); /* main() should have set progname. If it didn't, we don't strip * the leading / as this is presumably a testing and not a production * version... */ if (!progname || !*progname) { progname = argv[0]; } /* loop over arguments, looking for * -d -- set directory for database * -t -- set trace level, etc * * don't use getopts, as we'll be using that later in main(), * and it doesn't like being called twice. * As a result, main() should ignore the z: option. */ while (--argc > 0) { if (**++argv == '-' || **argv == '+') { char TurnOn = (**argv == '-'); switch((*argv)[1]) { case 'm': /* precision for matching */ argv[0][1] = 'z'; /* so it gets ignored by getopt */ if (!*(p = &argv[0][2])) { if (argc > 1) { argc--; argv++; p = (*argv); } else { Error(E_FATAL|E_XHINT, "-m must be followed by a, h or p" ); } } if (p[1]) { Error(E_FATAL|E_XHINT, "-m must be followed by a, h or p, not \"%s\"", p ); } switch (*p) { case 'p': /* precise */ Result->phrasematchlevel.Value = (int) PCM_SameCase; break; case 'h': /* heuristic */ Result->phrasematchlevel.Value = (int) PCM_HalfCase; break; case 'a': /* any, approxmate */ Result->phrasematchlevel.Value = (int) PCM_AnyCase; break; default: Error(E_FATAL|E_XHINT, "-m must be followed by p, h or a"); } break; case 'v': /* -v is the same as -t1 */ argv[0][1] = 'Z'; /* so it gets ignored by getopt */ if (LQT_SetTraceFlag(LQTRACE_VERBOSE)) { (void) LQT_SetTraceFlag(LQTRACE_DEBUG); } break; case 't': /* trace level */ argv[0][1] = 'z'; /* so it gets ignored by getopt */ if (argv[0][2] != '\0') { p = &argv[0][2]; } else if (argc > 1) { argc--; p = (*++argv); } else { p = "1"; } if (LQU_cknatstr(p)) { LQT_SetTraceFlag((t_TraceFlag) atoi(p)); } else if (STREQ(p, "list") || STREQ(p, "List")) { Error(E_USAGE|E_FATAL|E_MULTILINE, "trace %s: valid flags are as follows:", p ); LQT_ForEachTraceFlag(LQTp_PrintOneTraceFlag); Error(E_USAGE|E_FATAL|E_LASTLINE, "Combine flags with |, e.g. -t \"Trace|Debug\"" ); } else { char *e = LQT_SetTraceFlagsFromString(p); if (e && *e) { Error(E_FATAL|E_MULTILINE, "-t %*.*s>>%s: invalid combination of trace flags...", e - p, e - p, p, e ); Error(E_FATAL|E_MULTILINE, "Use -t list for a list of valid flags;" ); Error(E_FATAL|E_LASTLINE, "Combine flags with |, e.g. -t \"Trace|Debug\"" ); } } LQT_Trace(LQT_GetTraceFlags(), "-t: trace flags set: 0%o/%s", LQTp_AsciiTraceLevel, LQT_GetTraceFlagsAsString() ); break; case 'd': argv[0][1] = 'z'; /* so it gets ignored by getopt */ Result->directory.HowItWasSet = LQCF_Cmdline; if (argv[0][2] != '\0') { Result->directory.Value = &argv[0][2]; } else { if (argc > 1) { Result->directory.Value = argv[1]; argc--; argv++; } else { Error(E_FATAL|E_XHINT, "%cd must be followed by a directory name", TurnOn ? '-' : '+' ); } } break; } /* end switch */ } else { /* not an option, so stop looking */ break; } } /* end while */ /* now we have parsed the command line arguments, so look for the * default directory */ if (Result->directory.HowItWasSet == LQCF_Default) { char *t; if ((t = getenv("LQTEXTDIR")) != (char *) 0) { Result->directory.Value = emalloc( "LQTEXTDIR", U32(strlen(t) + 1) ); (void) strcpy(Result->directory.Value, t); Result->directory.HowItWasSet = LQCF_Envvar; } else { #ifdef UNDERHOME char *home = LQU_GetLoginDirectory(); if (home) { Result->directory.Value = LQU_joinstr3(home, "/", UNDERHOME); Result->directory.HowItWasSet = LQCF_Envvar; (void) efree(home); } else { Error(E_FATAL, "can't find your login directory ($HOME)"); } #endif /* UNDERHOME*/ } } if (!Result->directory.Value) { Error(E_FATAL, "Can't find database directory; $LQTEXTDIR and $HOME unset" ); } return Result; } /* * LQT_PrintDefaultUsage * Database/Defaults * * Prints to stderr a usage message that describes command-line options * specific to (and interpreted by) liblqtext. * You should call this if an unknown command-line option was found, * other than -z or -Z. * * This routine will change in the next release, with an entirely * new argument processing mechanism. * * LQT_InitFromArgv * */ API void LQT_PrintDefaultUsage(Options) t_lqdbOptions *Options; { fprintf(stderr, "\ -d dir -- use the lq-text database in the named directory\n\ -m c -- set matching criteria -- c is \"p\", \"h\" or \"a\"\n"); if (LQT_TraceFlagsSet(LQTRACE_VERBOSE)) { fprintf(stderr, "\ -m p uses precise matching, where CaSe is significant;\n\ -m h uses heuristic matching, which is the default;\n\ -m a uses approximate matching.\n" ); } fprintf(stderr, "\n\ -t N -- set trace level t N (default is zero)\n\ -t str -- set the trace flags in str (e.g. -t Verbose|Debug)\n\ -x -- print an explanation\n\ -xv -- print a longer explanation\n\ -V -- print version information\n\ -v -- be verbose (same as -t 1)\n" ); } PRIVATE char * FromWhereString(Whence) t_FromWhere Whence; { char *msg = ""; switch (Whence) { case LQCF_Cmdline: msg = " (specified with the -d option)"; break; case LQCF_Envvar: msg = " (from $LQTEXTDIR)"; break; case LQCF_Default: msg = " (compiled-in default)"; break; case LQCF_File: msg = " (from the database configuration file)"; break; case LQCF_None: msg = " (disabled by + option)"; break; default: break; } return msg; } #define LCNOMAP 0 /* Token -- leave case alone */ #define LCMAP 1 /* map to lower case */ static int RMLine = 0; static char *Bool_Yes[] = { "on", "yes", "true", "#t", /* for scheme */ "t", "1", 0 }; static char *Bool_No[] = { "off", "no", "false", "#f", "()", "nil", "0", 0 }; PRIVATE int StringToBool(String) char *String; { char *tmp = emalloc("StringToBool", U32(strlen(String) + 1)); register char *p, *q; int i; p = String; q = tmp; while (*p) { if (isascii(*p) && isupper(*p)) { *q = tolower(*p); } else { *q = *p; } q++; p++; } *q = '\0'; for (i = 0; Bool_Yes[i] != (char *) 0; i++) { if (STREQ(Bool_Yes[i], tmp)) { (void) efree(tmp); return 1; } } for (i = 0; Bool_No[i] != (char *) 0; i++) { if (STREQ(Bool_No[i], tmp)) { (void) efree(tmp); return 0; } } return -1; } /* * LQT_GetOption * Database/Defaults * *

    This function returns the value of a configuration option. * The options at present include: * *

  • directory, which is * the name of the directory containing the lq-text database;
  • *
  • stop list, which is the name of a file containing words * that are not indexed;
  • *
  • file search path, which is a colon-separated * list of directories that are searched for documents during indexing * and retrieval, and
  • *
  • phrase match level, which determined how precisely * phrases are matched.
  • * * * A pointer to the actual value; do not free this value. * * LQT_OpenDatabase *
    */ API void * LQT_GetOption(Options, Name) t_lqdbOptions *Options; char *Name; { /* TODO: use a linked list and search it! */ if (!Name || !*Name || !Options) { Error(E_BUG, "Attempt to retrieve null string from options"); } if (STREQ(Name, Options->directory.Name)) { return Options->directory.Value; } if (STREQ(Name, Options->stoplist.Name)) { return Options->stoplist.Value; } if (STREQ(Name, Options->filesearchpath.Name)) { return Options->filesearchpath.Value; } if (STREQ(Name, Options->phrasematchlevel.Name)) { return (void *) Options->phrasematchlevel.Value; } if (STREQ(Name, Options->locale.Name)) { return (void *) Options->locale.Value; } return 0; } PRIVATE char *LQTpGetNextToken( #ifdef HAVE_PROTO FILE *fd, char *Name, int Map #endif ); LIBRARY void LQTpReadDefaultFile(db) t_LQTEXT_Database *db; { char *ReadMe; FILE *fp = (FILE *) 0; char *Token; if (!LQU_IsDir(db->DatabaseDirectory)) { Error(E_FATAL|E_SYS, "database directory \"%s\" does not exist" ); } ReadMe = LQU_joinstr3(db->DatabaseDirectory, "/", "config.txt"); if (!LQU_IsFile(ReadMe)) { efree(ReadMe); ReadMe = LQU_joinstr3(db->DatabaseDirectory, "/", "readme"); } if (!LQU_IsFile(ReadMe)) { efree(ReadMe); ReadMe = LQU_joinstr3(db->DatabaseDirectory, "/", "README"); } fp = LQU_fEopen(E_FATAL|E_SYS, ReadMe, "database configuration file", "r" ); /* Read README up to an "end" line, ignoring lines starting with # */ while ((Token = LQTpGetNextToken(fp, ReadMe, LCMAP)) != (char *) 0) { if (STREQ(Token, "end")) { break; } if (STREQ(Token, "common") || STREQ(Token, "stoplist")) { if (!(Token = LQTpGetNextToken(fp, ReadMe, LCNOMAP))) { Error(E_FATAL, "%s %d: unexpected eof at common file", ReadMe, RMLine ); } else if (db->Options.stoplist.HowItWasSet == LQCF_Default) { db->Options.stoplist.Value = emalloc( Token, U32(strlen(Token) + 1) ); (void) strcpy(db->Options.stoplist.Value, Token); db->Options.stoplist.HowItWasSet = LQCF_File; } } else if (STREQ(Token, "echo")) { char *p = LQTpGetNextToken(fp, ReadMe, LCNOMAP); if (!p) p = ""; fprintf(stderr, "%s: %s", progname, p); } else if (STREQ(Token, "minwordlength") || STREQ(Token, "minlen")) { if (!(Token = LQTpGetNextToken(fp, ReadMe, LCNOMAP))) { Error(E_FATAL, "%s: %d: unexpected eof at common file", ReadMe, RMLine ); } else if (LQU_cknatstr(Token)) { db->MinWordLength = atoi(Token); } else { Error(E_FATAL, "config file %s: %d: minwordlength must be followed by a number.", ReadMe, RMLine ); } } else if (STREQ(Token, "wordlist") || STREQ(Token, "wordsinindex")) { if (!(Token = LQTpGetNextToken(fp, ReadMe, LCNOMAP))) { Error(E_FATAL, "Config file %s: %d: expected on or off", ReadMe, RMLine ); } else { switch (StringToBool(Token)) { case 0: db->WordsInWordIndex = 0; break; case 1: db->WordsInWordIndex = 1; break; case -1: Error(E_FATAL, "Config file %s: %d: wordlist must be followed by on or off, not %s", ReadMe, RMLine, Token ); } } } else if (STREQ(Token, "maxwordlength") || STREQ(Token, "maxlen")) { if (!(Token = LQTpGetNextToken(fp, ReadMe, LCNOMAP))) { Error(E_FATAL, "%s: %d: unexpected eof at common file", ReadMe, RMLine ); } else if (LQU_cknatstr(Token)) { db->MaxWordLength = atoi(Token); } else { Error(E_FATAL, "config file %s: %d: maxwordlength must be followed by a number.", ReadMe, RMLine ); } } else if (STREQ(Token, "fileblocksize")) { if (!(Token = LQTpGetNextToken(fp, ReadMe, LCNOMAP))) { Error(E_FATAL, "%s: %d: unexpected eof at FileBlockSize", ReadMe, RMLine ); } else if (LQU_cknatstr(Token)) { db->FileBlockSize = atoi(Token); } else { Error(E_FATAL, "config file %s: %d: FileBlockSize must be followed by a number.", ReadMe, RMLine ); } } else if (STREQ(Token, "path") || STREQ(Token, "docpath")) { if (!(Token = LQTpGetNextToken(fp, ReadMe, LCNOMAP))) { Error(E_FATAL, "%s: %d: unexpected end of file in docpath line", ReadMe, RMLine ); } else { db->DocPath = emalloc("DocPath", U32(strlen(Token) + 1)); (void) strcpy(db->DocPath, Token); /* DocFromWhere = LQCF_File; */ } } else if (STREQ(Token, "numbers") || STREQ(Token, "indexnumbers")) { if (!(Token = LQTpGetNextToken(fp, ReadMe, LCNOMAP))) { Error(E_FATAL, "Config file %s: %d: expected on or off", ReadMe, RMLine ); } else { switch (StringToBool(Token)) { case 0: db->IndexNumbers = 0; break; case 1: db->IndexNumbers = 1; break; case -1: Error(E_FATAL, "Config file %s: %d: indexnumbers must be followed by on or off, not %s", ReadMe, RMLine, Token ); } } } else if (STREQ(Token, "convertnumbers")) { if (!(Token = LQTpGetNextToken(fp, ReadMe, LCNOMAP))) { Error(E_FATAL, "Config file %s: %d: expected on or off", ReadMe, RMLine ); } else { switch (StringToBool(Token)) { case 0: db->ConvertNumbers = 0; break; case 1: db->ConvertNumbers = 1; break; case -1: Error(E_FATAL, "Config file %s: %d: ConvertNumbers must be followed by on or off, not %s", ReadMe, RMLine, Token ); } } } else if (STREQ(Token, "ignorehtmlhead")) { if (!(Token = LQTpGetNextToken(fp, ReadMe, LCNOMAP))) { Error(E_FATAL, "Config file %s: %d: expected on or off", ReadMe, RMLine ); } else { switch (StringToBool(Token)) { case 0: db->IgnoreHTMLhead = 0; break; case 1: db->IgnoreHTMLhead = 1; break; case -1: Error(E_FATAL, "Config file %s: %d: ignoreHTMLhead must be followed by on or off, not %s", ReadMe, RMLine, Token ); } } } else if (STREQ(Token, "title")) { if (!(Token = LQTpGetNextToken(fp, ReadMe, LCNOMAP))) { Error(E_FATAL, "%s: %d: unexpected eof at database title", ReadMe, RMLine ); } else { db->Options.title.Value = emalloc( Token, U32(strlen(Token) + 1) ); (void) strcpy(db->Options.title.Value, Token); } } else if (STREQ(Token, "administrator_email")) { if (!(Token = LQTpGetNextToken(fp, ReadMe, LCNOMAP))) { Error(E_FATAL, "%s: %d: unexpected eof at administrator_email", ReadMe, RMLine ); } else { db->Options.administrator_email.Value = emalloc( Token, U32(strlen(Token) + 1) ); (void) strcpy(db->Options.administrator_email.Value, Token); } } else if (STREQ(Token, "data_owner_email")) { if (!(Token = LQTpGetNextToken(fp, ReadMe, LCNOMAP))) { Error(E_FATAL, "%s: %d: unexpected eof at data_owner_email", ReadMe, RMLine ); } else { db->Options.data_owner_email.Value = emalloc( Token, U32(strlen(Token) + 1) ); (void) strcpy(db->Options.data_owner_email.Value, Token); } } else if (STREQ(Token, "locale")) { if (!(Token = LQTpGetNextToken(fp, ReadMe, LCNOMAP))) { Error(E_FATAL, "%s: %d: unexpected eof at locale", ReadMe, RMLine ); } else { db->Options.locale.Value = emalloc( Token, U32(strlen(Token) + 1) ); (void) strcpy(db->Options.locale.Value, Token); } } else if (STREQ(Token, "wordflags")) { if (!(Token = LQTpGetNextToken(fp, ReadMe, LCNOMAP))) { Error(E_FATAL, "%s: %d: unexpected eof at wordflags", ReadMe, RMLine ); } else { db->Options.wordflags.Value = emalloc( Token, U32(strlen(Token) + 1) ); (void) strcpy(db->Options.wordflags.Value, Token); } } else if ( STREQ(Token, "url") || STREQ(Token, "cgi-url") || STREQ(Token, "cgiurl") || STREQ(Token, "cgi_url") ) { if (!(Token = LQTpGetNextToken(fp, ReadMe, LCNOMAP))) { Error(E_FATAL, "%s: %d: unexpected eof at database url", ReadMe, RMLine ); } else { db->Options.cgi_url.Value = emalloc( Token, U32(strlen(Token) + 1) ); (void) strcpy(db->Options.cgi_url.Value, Token); } } else { Error(E_FATAL, "\"%s\": %d: \"%s\" unexpected (use # to start a comment)", ReadMe, RMLine, Token ); } } /* while */ if (fclose(fp) == EOF) { Error(E_WARN|E_SYS, "couldn't close file \"%s\"", ReadMe); } /* check values are OK */ if (db->WordsInWordIndex && (db->MaxWordLength + 10 > WIDBLOCKSIZE)) { Error(E_WARN, "Config file %s: Max wordlength %d reduced to %d (WIDBLOCKSIZE %d)", ReadMe, db->MaxWordLength, WIDBLOCKSIZE - 10, WIDBLOCKSIZE ); db->MaxWordLength = WIDBLOCKSIZE - 10; } /* generate wordflags value */ { unsigned long FlagObject; char *p; p = LQT_StringToWordFlags( db, db->Options.wordflags.Value, &FlagObject ); if (p && *p) { Error(E_FATAL|E_MULTILINE, "unrecognised value for %s%s", db->Options.wordflags.Name, FromWhereString(db->Options.wordflags.HowItWasSet) ); Error(E_FATAL|E_MULTILINE|E_LASTLINE, "%s: %*.*s>>%s", db->Options.wordflags.Name, p - db->Options.wordflags.Value, p - db->Options.wordflags.Value, db->Options.wordflags.Value, p ); } db->WordFlags = FlagObject; db->WordFlags |= WPF_LASTINBLOCK; #ifdef ASCIITRACE if (LQT_TraceFlagsSet(LQTRACE_DEBUG)) { LQT_Trace(LQTRACE_DEBUG, "WordFlags set to %s\n", LQT_WordFlagsToString(db, (t_WordFlags) db->WordFlags) ); } #endif } (void) efree(ReadMe); return; } PRIVATE char * LQTpGetNextToken(fd, Name, Map) FILE *fd; char *Name; int Map; { int ch; static char *buf = (char *) NULL; static unsigned int BytesAllocated = 0; register char *q = buf; int InQuote = 0; int OriginalMap = Map; if (!buf || !BytesAllocated) { BytesAllocated = 50; buf = emalloc("LQTpGetNextToken line buffer", BytesAllocated); } q = buf; while ((ch = NextChar(fd, Name, Map)) != EOF) { /* Ensure that we don't fall off the end of the buffer */ if (q - buf >= BytesAllocated) { int WhereWeWere = q - buf; BytesAllocated += 50; buf = erealloc(buf, BytesAllocated); q = &buf[WhereWeWere]; } switch (ch) { case '"': case '\'': if (!InQuote) { if (q == buf) { /* start a quoted string */ InQuote = ch; Map = 0; /* no case conversion inside strings */ } else { Error(E_FATAL, "%s: %d: character (%c) must be quoted", Name, RMLine, ch ); } } else { if (ch == InQuote) { /* end of the string */ *q = '\0'; return buf; } else { /* nested quote: "....'..." */ *q++ = ch; } } break; case '\\': /* swallow the backslash: */ if ((ch = NextChar(fd, Name, Map)) == EOF) { Error(E_FATAL, "%s; %d: EOF after \\ unexpected!", Name, RMLine ); } *q++ = ch; break; case ' ': if (InQuote) { *q++ = ch; break; } /* ELSE FALL THROUGH */ case '\n': if (InQuote) { *q = '\0'; Error(E_FATAL, "%s: %d: missing quote after %c%s", Name, RMLine, InQuote, buf ); } if (q - buf >= BytesAllocated) { int WhereWeWere = q - buf; BytesAllocated += 50; buf = erealloc(buf, BytesAllocated); q = &buf[WhereWeWere]; } *q = '\0'; if (q > buf) { return buf; } else { return LQTpGetNextToken(fd, Name, OriginalMap); } /*NOTREACHED*/ break; default: *q++ = ch; break; } } if (q > buf) { Error(E_FATAL, "%s: %d: unexpected end of file", Name, RMLine); } (void) efree(buf); buf = 0; BytesAllocated = 0; return (char *) 0; } PRIVATE int NextChar(fd, Name, Map) FILE *fd; char *Name; int Map; { int ch; while ((ch = getc(fd)) != EOF) { switch (ch) { case '#': do { if ((ch = getc(fd)) == EOF) { Error(E_FATAL, "%s: %d: unexpected end of file inside comment", Name, RMLine ); } } while (ch != '\n'); /* ASSERT: ch == '\n' */ ++RMLine; break; case '\n': ++RMLine; return ch; case ' ': case '\t': case '\f': case '\r': if (Map) { return ' '; } else { return ch; } default: return (Map && isupper(ch)) ? tolower(ch) : ch; } } /* while */ return EOF; }