/* #ident "@(#)smail/src:RELEASE-3_2_0_121:parse.c,v 1.34 2005/07/06 23:56:41 woods Exp" */ /* * Copyright (C) 1987, 1988 by Ronald S. Karr and Landon Curt Noll * Copyright (C) 1992 Ronald S. Karr * * See the file COPYING, distributed with smail, for restriction * and warranty information. */ /* * parse.c: (XXX this source file should be renames parsecfg.c!) * Parse configuration files in a standard way. * * The directory, router and transport files all share a common format * which are parsed using routines in this file. Although the format is * slightly different, the rules for lexical tokens in the config files, * as well as those for the method, retry, and qualify tables, are very * much the same, so routines for parsing these files are provided as * well. The basic read_entry() and skip_space() routines are also used * by the "lsearch" database lookup protocol, as well as the queryprog * router driver. * * external functions: parse_entry, parse_config, parse_table, * read_entry, skip_space. */ #include "defs.h" #ifdef STANDALONE # define xmalloc malloc # define xrealloc realloc # define xfree free #endif /* STANDALONE */ #include #include #include #include #ifdef STDC_HEADERS # include # include #else # ifdef HAVE_STDLIB_H # include # endif #endif #ifdef HAVE_STRING_H # if !defined(STDC_HEADERS) && defined(HAVE_MEMORY_H) # include # endif # include #endif #ifdef HAVE_STRINGS_H # include #endif #ifdef __STDC__ # include #else # include #endif #if defined(HAVE_UNISTD_H) # include #endif #include "smail.h" #include "alloc.h" #include "list.h" #include "main.h" #include "parse.h" #include "addr.h" #include "log.h" #include "smailstring.h" #include "dys.h" #include "exitcodes.h" #include "debug.h" #include "extern.h" #include "smailport.h" /* variables exported from this file */ char *on = "1"; /* boolean on attribute value */ char *off = "0"; /* boolean off attribute value */ /* functions local to this file */ static char *finish_parse __P((char *, struct attribute **, struct attribute **)); /* * parse_entry - parse an entry from director, router or transport file * * given an entry from the given file, parse that entry, returning a vector * containing the NUL-terminated name of the entry followed by the name/value * pairs for all of the attributes in the entry. Names which fit the form of a * boolean attribute will return a pointer to one of the external variables * "on" or "off". * * Returns the name of the entry on a successful read. Returns NULL * on end of file or error. For an error, a message is returned as an * error string. Calling xfree on the name is sufficient to free all * of the string storage. To free the list entries, step through the * lists and free each in turn. * * Note this code is very similar to the code in parse_config(). */ char * parse_entry(entry, generic, driver, error) register char *entry; /* entry string to be parsed */ struct attribute **generic; /* all generic attributes */ struct attribute **driver; /* all driver attributes */ char **error; /* returned error message */ { char *start; struct str str; /* string for processing entry */ int this_attribute; /* offset of attribute name */ DEBUG2(DBG_CONF_HI, "parse_entry(): about to process entry that begins with: \n\t'%V'\n", (size_t) 70, entry); /* skip leading whitespace and comments */ entry = skip_space(entry); *error = NULL; /* no error yet! */ start = entry; STR_INIT(&str); /* * grab the entry name as a collection of characters followed * by optional white space followed by a `:' */ while (*entry && !isspace((int) *entry) && *entry != ':' && *entry != '#') { STR_NEXT(&str, *entry++); } STR_NEXT(&str, '\0'); /* NUL-terminate with the name */ if (STR_LEN(&str) == 0) { *error = xprintf("missing field name (after entry beginning with \"%Q\")", (size_t) 40, start); DEBUG1(DBG_CONF_LO, "parse_entry:() %s\n", *error); STR_FREE(&str); return NULL; } /* skip spaces and comments after the name */ entry = skip_space(entry); if (*entry != ':') { *error = "field name does not end in `:'"; DEBUG1(DBG_CONF_LO, "parse_entry:() %s\n", *error); STR_FREE(&str); return NULL; } /* skip the `:' */ entry++; /* * loop grabbing attributes and values until the end of * the entry * * XXX this code is very similar to the code below in parse_config() */ while (*entry) { size_t name_offset; /* temp */ int boolean_flag; /* `+' `-' or SPACE */ boolean_flag = ' '; start = entry; /* skip spaces and comments before the attribute name (or `;') */ entry = skip_space(entry); /* a semicolon separates generic and driver attributes */ if (*entry == ';') { /* skip spaces and comments after the semicolon */ entry = skip_space(entry + 1); /* and if there's still anything afterward, record the semicolon */ if (*entry) { STR_NEXT(&str, ';'); } } /* * be lenient about a `;' (or `,' from last go-around) with no * following attributes */ if (*entry == '\0') { break; } /* remember where this attribute setting starts in the parsed copy */ this_attribute = STR_LEN(&str); /* if it's a boolean attribute then copy the value verbatim */ if (*entry == '+' || *entry == '-') { boolean_flag = *entry; STR_NEXT(&str, *entry); /* skip any spaces and comments after the flag */ entry = skip_space(entry + 1); } if (*entry == '\0' || *entry == ',' || *entry == ';') { *error = xprintf("in entry %v: missing attribute name%s, in text beginning with \"%Q\"", STR(&str), boolean_flag != ' ' ? " after boolean flag" : "", (size_t) 40, start); DEBUG1(DBG_CONF_LO, "parse_entry:() %s\n", *error); STR_FREE(&str); return NULL; } /* * scan over the attribute name and copy it to str * * an attribute name is of the form [+-]?[A-Za-z0-9_-]+ * * XXX should probably require it to start with isalpha()... */ name_offset = STR_LEN(&str); while (*entry && (isalnum((int) *entry) || *entry == '_' || *entry == '-')) { STR_NEXT(&str, *entry++); } if (STR_LEN(&str) == name_offset) { *error = xprintf("missing variable name%s, in entry beginning with \"%Q\"", boolean_flag != ' ' ? " after boolean flag" : "", (size_t) 40, start); DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error); return NULL; } STR_NEXT(&str, '\0'); /* terminate the attribute name */ /* skip any spaces and comments after the end of the name */ entry = skip_space(entry); if (*entry == '\0' || *entry == ',' || *entry == ';') { /* end of a boolean attribute */ if (*entry == ',') { /* skip over commas */ entry++; } continue; } if (*entry != '=') { /* not boolean form and not "name = value" form */ *error = xprintf("in entry %v: attribute %v: expected `=' after the variable name", STR(&str), STR(&str) + this_attribute); DEBUG1(DBG_CONF_LO, "parse_entry:() %s\n", *error); STR_FREE(&str); return NULL; } if (boolean_flag != ' ') { /* * if we found a leading `+' or `-' then there shouldn't be an * assignment, but there was.... */ *error = xprintf("in entry %v: attribute %v: unexpected pattern `= value' follows boolean variable", STR(&str), STR(&str) + this_attribute); DEBUG1(DBG_CONF_LO, "parse_entry:() %s\n", *error); STR_FREE(&str); return NULL; } /* note that this is a attribute value assignment */ STR_NEXT(&str, '='); /* skip any spaces and comments after the '=' */ entry = skip_space(entry + 1); if (*entry == '"') { entry++; /* skip the opening " */ /* * if a quote, skip to the closing quote, following standard * C convention with \-escapes. Note that read_entry will * have already done some processing for \ chars at the end of * input lines. */ while (*entry && *entry != '"') { if (*entry == '\\') { int c; entry = c_dequote(entry + 1, &c); STR_NEXT(&str, c); } else { STR_NEXT(&str, *entry++); } } if (*entry != '"') { /* * make sure that the string doesn't suddenly come * to an end at a funny spot */ *error = xprintf("in entry %v: attribute %v: missing closing quote for quoted value", STR(&str), STR(&str) + this_attribute); DEBUG1(DBG_CONF_LO, "parse_entry:() %s\n", *error); STR_FREE(&str); return NULL; } entry++; /* skip that closing '"' */ } else { /* * not in double quotes, only a limited set of characters * are allowed in an unquoted string, though \ quotes any * character. */ while (*entry && (*entry == '\\' || strchr("!@$%^&*-_+~/?|<>:[]{}().`'", *entry) || isalnum((int) *entry))) { if (*entry == '\\') { entry++; if (*entry == '\0') { /* must have something after \ */ *error = xprintf("in entry %v: attribute %v: unexpected end of unquoted value after a backslash", STR(&str), STR(&str) + this_attribute); DEBUG1(DBG_CONF_LO, "parse_entry:() %s\n", *error); STR_FREE(&str); return NULL; } } STR_NEXT(&str, *entry++); } /* note we check the proper termination below.... */ } STR_NEXT(&str, '\0'); /* close off the value */ /* skip any spaces and comments after the value */ entry = skip_space(entry); /* * make sure the entry ends in something reasonable */ if (*entry == ',') { entry++; /* commas are okay, and are ignored */ } else if (*entry != ';') { ; /* semicolons are OK too, but are not ignored */ } else if (*entry != '\0') { /* anything else is not OK */ *error = xprintf("in entry %v: attribute %v: invalid attribute separator (%c)", STR(&str), STR(&str) + this_attribute, *entry); DEBUG1(DBG_CONF_LO, "parse_entry:() %s\n", *error); STR_FREE(&str); return NULL; } } STR_NEXT(&str, '\0'); /* two NUL bytes signal the entry's end */ STR_DONE(&str); /* finish off the string */ /* * turn the entry, after the entry name, into the finished attribute lists */ *error = finish_parse(STR(&str) + strlen(STR(&str)) + 1, generic, driver); if (*error) { DEBUG1(DBG_CONF_LO, "parse_entry:() finish_parse(): %s\n", *error); STR_FREE(&str); return NULL; /* error found in finish_parse */ } return STR(&str); /* entry name was first */ } /* * finish_parse - turn NUL-separated token strings into an attribute list * * return an error message or NULL, return generic and driver attributes * in the appropriate passed list pointers. */ static char * finish_parse(tokens, generic, driver) register char *tokens; /* strings of nul-terminated tokens */ struct attribute **generic; /* generic attributes go here */ struct attribute **driver; /* driver attributes go here */ { struct attribute **attr = generic; /* begin adding generic attributes */ *generic = NULL; *driver = NULL; /* * loop, snapping up tokens until no more remain */ while (*tokens) { struct attribute *new; if (*tokens == ';') { /* after `;' parse driver attributes */ attr = driver; tokens++; /* otherwise ignore `;' */ } /* * get a new token and link it into the output list */ new = (struct attribute *) xmalloc(sizeof(*new)); new->succ = *attr; (*attr) = new; /* fill in the name */ new->name = tokens; /* step to the next token */ tokens = tokens + strlen(tokens) + 1; /* check for boolean attribute form */ if (new->name[0] == '-' || new->name[0] == '+') { /* boolean value */ if (*tokens == '=') { /* XXX haven't we already checked for this? */ /* can't have both [+-] and a value */ /* note new attribute is already linked into the list */ return "mixed [+-]attribute and value assignment"; } /* * -name turns off attribute, +name turns it on */ if (new->name[0] == '-') { new->value = off; } else { new->value = on; } new->name++; /* don't need [+-] anymore */ } else { if (*tokens == '=') { /* value token for attribute */ new->value = tokens + 1; /* don't include `=' in the value */ /* advance to the next token */ tokens = tokens + strlen(tokens) + 1; } else { /* just name is equivalent to +name */ new->value = on; } } } return NULL; } /* * parse_config - parse config file name/value pairs * * given a string, such as returned by read_entry, turn it into a single * attribute entry with the "name" field pointing at the start of the config * variable name and with the "value" field pointing at the variable's value. * Both are pointing at the same allocated storage block and can be freed by * passing the "name" pointer to xfree(). * * On error, return NULL, with an error message in *error. * * XXX this code is very similar to the inner loop in parse_entry() above... */ struct attribute * parse_config(entry, error) register char *entry; /* config from read_entry */ char **error; /* return error message */ { char *start = entry; struct str str; /* area for building result */ int boolean_flag = ' '; /* `+' `-' or SPACE */ int value_offset; /* offset in STR(&str) of value */ struct attribute *attr; /* new variable name/value struct */ DEBUG2(DBG_CONF_HI, "parse_config(): about to process entry that begins with:\n\t'%V'\n", (size_t) 70, entry); /* variable setting begins at next non-white space character */ entry = skip_space(entry); /* is this a boolean variable setting? */ if (*entry == '+' || *entry == '-') { boolean_flag = *entry; /* skip any spaces and comments after the flag */ entry = skip_space(entry + 1); } if (*entry == '\0') { *error = xprintf("missing variable name%s, in entry beginning with \"%Q\"", boolean_flag != ' ' ? " after boolean flag" : "", (size_t) 40, start); DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error); return NULL; } attr = (struct attribute *) xmalloc(sizeof(*attr)); attr->succ = NULL; STR_INIT(&str); /* * scan over the variable name and copy it to str * * we assume there's at least one valid non-space character here * * attribute name is of the form [A-Za-z0-9_-]+ * * XXX should probably require it to start with isalpha()... */ while (*entry && (isalnum((int) *entry) || *entry == '_' || *entry == '-')) { STR_NEXT(&str, *entry++); } if (STR_LEN(&str) == 0) { *error = xprintf("missing variable name%s, in entry beginning with \"%Q\"", boolean_flag != ' ' ? " after boolean flag" : "", (size_t) 40, start); DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error); return NULL; } STR_NEXT(&str, '\0'); /* terminate variable name */ /* skip any spaces and comments after the end of the name */ entry = skip_space(entry); if (*entry == '\0') { /* boolean variable */ STR_DONE(&str); attr->name = STR(&str); if (boolean_flag == '-') { attr->value = off; } else { attr->value = on; } return attr; } /* not boolean form and not "name = value" form */ if (*entry != '=') { *error = xprintf("variable %v: expected `=' after the variable name", STR(&str)); DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error); xfree((char *) attr); STR_FREE(&str); return NULL; } if (boolean_flag != ' ') { /* * if we found a leading `+' or `-' then there shouldn't be an * assignment, but there was.... */ *error = xprintf("variable %s: unexpected pattern `= value' follows a boolean variable", STR(&str)); DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error); xfree((char *) attr); STR_FREE(&str); return NULL; } /* skip any spaces after the '=' */ entry = skip_space(entry + 1); /* form is `name = value', remember the start position of the value */ value_offset = STR_LEN(&str); /* * XXX this code is very nearly a duplicate of the code above in parse_entry() */ if (*entry == '"') { entry++; /* skip the opening " */ /* * if a quote, skip to the closing quote, following standard * C convention with \-escapes. Note that read_entry will * have already done some processing for \ chars at the end of * input lines. */ while (*entry && *entry != '"') { if (*entry == '\\') { int c; entry = c_dequote(entry + 1, &c); STR_NEXT(&str, c); } else { STR_NEXT(&str, *entry++); } } if (*entry != '"') { /* * make sure that the string doesn't suddenly come * to an end at a funny spot, normally it will be a NUL. */ *error = xprintf("variable %v: missing closing quote for quoted value", STR(&str)); DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error); xfree((char *) attr); STR_FREE(&str); return NULL; } entry++; /* skip that closing '"' */ } else { /* * not in double quotes, only a limited set of characters are allowed * in an unquoted variable string (more than in an attribute string), * though backslash (\) still quotes any character. * * Note too that in a config file we allow continuation of variable * values without a trailing backslash. */ while (*entry && (*entry == '\\' || *entry == '\n' || strchr(" \t!@$%^&*-_=+~/?|<>;:[]{}(),.`'", *entry) || /* XXX " \t;,=" added */ isalnum((int) *entry))) { if (*entry == '\n') { entry = skip_space(entry); if (*entry == '\0') { break; /* normal end of entry... */ } } if (*entry == '\\') { entry++; if (*entry == '\0') { /* must have something after \ */ *error = xprintf("variable %v: unexpected end of unquoted value after a backslash", STR(&str)); DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error); xfree((char *) attr); STR_FREE(&str); return NULL; } } STR_NEXT(&str, *entry++); } if (*entry != '\0') { *error = xprintf("variable %v: unexpected, unescaped, special character '%c' in value, next 40 bytes of remaining text: \"%Q\"", STR(&str), *entry, (size_t) 40, entry); DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error); xfree((char *) attr); STR_FREE(&str); return NULL; } } STR_NEXT(&str, '\0'); /* close off the string after the value */ /* * make sure this is really the end of the entry, first eating any trailing * whitespace and comments that would be ignored anyway.... */ entry = skip_space(entry); if (*entry != '\0') { *error = xprintf("variable %v: unexpected data after end of entry, next 40 bytes of remaining text: \"%Q\"", STR(&str), (size_t) 40, entry); DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error); xfree((char *) attr); STR_FREE(&str); return NULL; } STR_DONE(&str); attr->name = STR(&str); attr->value = STR(&str) + value_offset; DEBUG3(DBG_CONF_HI, "parse_config(): got var `%v', with value that begins with:\n\t'%V'\n", attr->name, (size_t) 70, attr->value); return attr; } /* * parse_table - parse an entry in a table file * * table files have entries of the form: * * string1 string2 * * the returned "struct attribute" its "name" field pointing at the beginning * of string1 and its "value" field pointing at the beginning of string2. Both * are pointing at the same allocated storage block and can be freed by passing * the "name" pointer to xfree(). */ struct attribute * parse_table(entry, error) register char *entry; /* config from read_entry */ char **error; /* return error message */ { struct attribute *attr = (struct attribute *)xmalloc(sizeof(*attr)); struct str str; int offset_transport; /* offset to transport in STR(&str) */ attr->succ = NULL; STR_INIT(&str); entry = skip_space(entry); while (*entry && !isspace((int) *entry) && *entry != '#') { STR_NEXT(&str, *entry++); } STR_NEXT(&str, '\0'); /* terminate name of host */ entry = skip_space(entry); if (*entry == '\0') { *error = "unexpected end of entry"; STR_FREE(&str); return NULL; } offset_transport = STR_LEN(&str); while (*entry && !isspace((int) *entry) && *entry != '#') { STR_NEXT(&str, *entry++); } STR_NEXT(&str, '\0'); /* terminate name of transport */ entry = skip_space(entry); if (*entry) { *error = "expected end of entry"; STR_FREE(&str); return NULL; } STR_DONE(&str); attr->name = STR(&str); attr->value = STR(&str) + offset_transport; return attr; } /* * skip_space - skip over comments and white space * * a comment is a `#' up to the end of a line */ char * skip_space(p) register char *p; /* current place in string */ { for (;;) { if (*p == '#') { /* skip over comment */ p++; while (*p && *p != '\n') { p++; } } else if (!isspace((int) *p)) { /* found something that isn't white space, return it */ return p; } else { p++; /* advance past the white-space char */ } } } /* * read_entry - read a standard, possibly multi-line, entry from a file * * A "standard" entry, e.g. from a config, director, router, or transport * configuration file, or an entry from an alias file, is terminated by a line * which does not begin with whitespace. * * All comments are included in the returned text -- only "\\\n[ ]*" (escaped * newlines followed by optional whitespace) are stripped. All comments at the * end of an entry are considered to be at the beginning of the next entry. * * return NULL on end of file or error. The region return may be * reused for subsequent return values and should be copied if it * is to be preserved. * * XXX maybe we should try to count newlines and provide the line number of the * last line read. That means we also need to keep track of the line number at * the trailing comment file position. Note however that parse_config() and * similar routines deal with entries as single objects and so there will still * be no way to determine the actual line number of a parsing error in the * middle of an entry. Perhaps the current technique of reporting the entry's * "name", as well as text leading up to (or following, as appropriate) the * error is good enough for users to find and understand the error in their raw * file format. */ char * read_entry(f, infn) register FILE *f; /* input file */ char *infn; /* input file name */ { register int c; /* current character */ static struct str str; /* build the entry here */ static int inited = FALSE; /* TRUE if str has been STR_INIT'd */ unsigned int ptcomment = 0; /* possible trailing comment offset */ fpos_t ptcompos; /* trailing comment file position */ /* * Note, that str is initialized only once and then reused. */ if (!inited) { inited = TRUE; STR_INIT(&str); } else { STR_CHECK(&str); STR_CLEAR(&str); } /* * scan for the beginning of an entry, which begins at the first * non-white space, non-comment character * * We do this in a separate loop since it makes the parsing of the entry * itself (to find its end) much easier (and also partly because once upon * a time this code skipped leading comments instead of including them in * the entry). */ while ((c = getc(f)) != EOF && (isspace((int) c) || c == '#')) { STR_NEXT(&str, c); if (c == '#') { while ((c = getc(f)) != EOF && c != '\n') { STR_NEXT(&str, c); } if (c == EOF) { break; } STR_NEXT(&str, c); /* include the end-of-comment */ } } /* * no entry was found */ if (c == EOF) { return NULL; } STR_NEXT(&str, c); /* * continue copying characters up to the end of the entry. */ while ((c = getc(f)) != EOF) { if (c == '\n') { STR_NEXT(&str, c); /* * peek ahead to see what the next line starts with */ c = getc(f); /* * end-of-file, or a line beginning with non-white space, marks the * end of the current entry. */ if (c == '\n' || c == '#') { /* blank lines and comments don't count */ (void) ungetc(c, f); /* unpeek */ /* * but we do want to remember where the blank lines or comments * start in case this turns out to be the end of the entry so * that we can trim them off and back up to read them as part * of the next entry.... * * Ideally we would like to trim only from a blank line (two * consecutive newlines) onwards, i.e. consider comments * "attached" to the end of the entry to be part of the entry, * but the logic to do that gets a bit too complicated to be * worthwhile. */ if (!ptcomment) { ptcomment = STR_LEN(&str); if (fgetpos(f, &ptcompos) != 0) { panic(EX_OSFILE, "fgetpos(%s): %s", infn, strerror(errno)); } } continue; /* keep reading */ } if (c == EOF || (c != ' ' && c != '\t')) { /* indeed this is the end of the entry! */ break; } else if (c != '#') { /* XXX does this do anything? can it? */ ptcomment = 0; } } if (c == '\\') { /* \newline is swallowed along with any following white-space */ if ((c = getc(f)) == EOF) { break; } if (c == '\n') { while ((c = getc(f)) == ' ' || c == '\t') { ; } } else { STR_NEXT(&str, '\\'); } } STR_NEXT(&str, c); } /* * that's the end of that entry */ if (c != EOF) { (void) ungetc(c, f); /* first character for the next entry? */ } /* * trim off any trailing blank lines and/or comments and back up to read * them as part of the next entry */ if (ptcomment) { STR_TRIM(&str, ptcomment); if (c != EOF) { if (fsetpos(f, &ptcompos) != 0) { panic(EX_OSFILE, "fsetpos(%s): %s", infn, strerror(errno)); } } } STR_NEXT(&str, '\0'); /* end of the entry */ return STR(&str); } #ifdef STANDALONE /* * read from standard input and write out the compiled * entry information on the standard output */ void main(argc, argv) int argc; /* count of arguments */ char *argv[]; /* vector of arguments */ { char *entry; /* entry read from stdin */ enum { config, table, other } file_type; /* type of file to look at */ if (argc >= 2 && EQ(argv[1], "-c")) { file_type = config; } else if (argc >= 2 && EQ(argv[1], "-t")) { file_type = table; } else { file_type = other; } /* * read entries until EOF */ while (entry = read_entry(stdin, "stdin")) { if (file_type == config) { char *error; struct attribute *attr = parse_config(entry, &error); if (attr == NULL) { (void) fprintf(stderr, "error in : %s\n", error); exit(EX_DATAERR); } (void) printf("%s = %s\n", attr->name, quote(attr->value)); } else if (file_type == table) { char *error; struct attribute *attr = parse_table(entry, &error); if (attr == NULL) { (void) fprintf(stderr, "error in : %s\n", error); exit(EX_DATAERR); } (void) printf("%s = %s\n", attr->name, quote(attr->value)); } else { struct attribute *generic; /* generic attribute list */ struct attribute *driver; /* driver attribute list */ char *error; /* error message */ char *name = parse_entry(entry, &generic, &driver, &error); if (name == NULL) { (void) fprintf(stderr, "error in : %s\n", error); exit(EX_DATAERR); } (void) printf("Entry Name: %s:\n Generic Attributes:\n", name); while (generic) { (void) printf("\t%s = %s\n", generic->name, quote(generic->value)); generic = generic->succ; } (void) printf(" Driver Attributes:\n"); while (driver) { (void) printf("\t%s = %s\n", driver->name, quote(driver->value)); driver = driver->succ; } } } exit(EX_OK); } #endif /* STANDALONE */ /* * Local Variables: * c-file-style: "smail" * End: */