/*
#ident	"@(#)smail/src:RELEASE-3_2_0_120:parse.c,v 1.30 2004/02/10 22:28:06 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:
 *	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 <sys/types.h>
#include <stdio.h>
#include <errno.h>
#include <ctype.h>

#ifdef STDC_HEADERS
# include <stdlib.h>
# include <stddef.h>
#else
# ifdef HAVE_STDLIB_H
#  include <stdlib.h>
# endif
#endif
#ifdef HAVE_STRING_H
# if !defined(STDC_HEADERS) && defined(HAVE_MEMORY_H)
#  include <memory.h>
# endif
# include <string.h>
#endif
#ifdef HAVE_STRINGS_H
# include <strings.h>
#endif
#ifdef __STDC__
# include <stdarg.h>
#else
# include <varargs.h>
#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 *eof_error __P((char *));
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
 * returning the name of the entry and 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.
 */
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 */
{
    struct str str;			/* string for processing entry */
    int this_attribute;			/* offset of attribute name */

    *error = NULL;			/* no error yet */
    STR_INIT(&str);

#if 0
    DEBUG1(DBG_CONF_HI, "parse_entry(): about to process <%v>\n", entry);
#endif

    /* skip leading whitespace and comments */
    entry = skip_space(entry);

    /*
     * 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++);
    }
    entry = skip_space(entry);

    if (*entry != ':') {
	*error = "field name does not end in `:'";
	return NULL;
    }
    STR_NEXT(&str, '\0');		/* done with the name */
    entry++;

    if (STR_LEN(&str) == 0) {
	*error = "null field name";
	return NULL;
    }

    /*
     * loop grabbing attributes and values until the end of
     * the entry
     */
    while (*entry) {
	size_t i;			/* temp */

	/* attribute name begins at next non-white space character */
	entry = skip_space(entry);

	if (*entry == ';') {
	    /* `;' separates generic and driver attributes */
	    entry = skip_space(entry + 1);
	    if (*entry) {
		STR_NEXT(&str, ';');
	    }
	}

	/* be lenient about a `;' or `,' with no following attributes */
	if (*entry == '\0') {
	    break;
	}

	/* attribute name is of the form [+-]?[A-Za-z0-9_-]+ */
	this_attribute = STR_LEN(&str);
	if (*entry == '+' || *entry == '-') {
	    STR_NEXT(&str, *entry++);
	}
	i = STR_LEN(&str);
	while (*entry && (isalnum((int) *entry) || *entry == '_' || *entry == '-')) {
	    STR_NEXT(&str, *entry++);
	}
	if (i == STR_LEN(&str)) {
	    *error = "null attribute name";
	    return NULL;
	}
	STR_NEXT(&str, '\0');		/* terminate the attribute name */
	entry = skip_space(entry);

	if (*entry == '\0' || *entry == ',' || *entry == ';') {
	    /* end of boolean attribute */
	    if (*entry == ',') {
		/* skip over commas */
		entry++;
	    }
	    continue;
	}
	if (*entry != '=') {
	    /* not boolean form and not "name = value" form */
	    *error = xprintf("no value for attribute %v",
			     STR(&str) + this_attribute);
	    return NULL;
	}

	/* note that this is a value */
	STR_NEXT(&str, '=');

	entry = skip_space(entry + 1);

	/*
	 * XXX this code is very nearly a duplicate of the code below in parse_config()
	 */
	if (*entry == '"') {
#if 0
	    DEBUG2(DBG_CONF_HI, "parse_entry(): attr %v: about to slurp quoted value: '%v'\n", STR(&str) + this_attribute, entry);
#endif
	    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.
	     */
	    /*
	     * XXX this loop is an exact duplicate of the code below in parse_config()
	     */
	    while (*entry && *entry != '"') {
		if (*entry == '\\') {
		    int c;

		    entry = c_dequote(entry + 1, &c);
		    STR_NEXT(&str, c);
		} else {
		    STR_NEXT(&str, *entry++);
		}
	    }
	    if (*entry == '\0') {
		/*
		 * make sure that the string doesn't suddenly come
		 * to an end at a funny spot
		 */
		*error = eof_error(STR(&str) + this_attribute);
		return NULL;
	    }
	    entry++;			/* skip the closing " */
	} else {
	    /*
	     * not in double quotes, only a limited set of characters
	     * are allowed in an unquoted string, though \ quotes any
	     * character.
	     */
#if 0
	    DEBUG2(DBG_CONF_HI, "parse_entry(): attr %v: about to slurp unquoted value: '%v'\n", STR(&str) + this_attribute, entry);
#endif
	    while (*entry && (*entry == '\\' ||
			      strchr("!@$%^&*-_+~/?|<>:[]{}().`'", *entry) ||
			      isalnum((int) *entry)))
	    {
		if (*entry == '\\') {
		    entry++;
		    if (*entry == '\0') {
			/* must have something after \ */
			*error = eof_error(STR(&str) + this_attribute);
			return NULL;
		    }
		}
		STR_NEXT(&str, *entry++);
	    }
	}
	STR_NEXT(&str, '\0');		/* close off 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 != '\0' && *entry != ';') {
	    /* end of string or ; separator are okay, anything else is not */
	    *error = xprintf("illegal attribute separator after %v",
			     STR(&str) + this_attribute);
	    return NULL;
	}
    }
    STR_NEXT(&str, '\0');		/* two nul bytes signal the end */
    STR_DONE(&str);			/* finish off the string */

    /*
     * turn all this into the finished tokens
     */
    *error = finish_parse(STR(&str) + strlen(STR(&str)) + 1, generic, driver);

    if (*error) {
	return NULL;			/* error found in finish_parse */
    }

    return STR(&str);			/* entry name was first */
}

/*
 * eof_error - form an unexpected eof error on the given attribute
 */
static char *
eof_error(name)
    char *name;				/* name of attribute */
{
    return xprintf("unexpected end of quoted string value for attribute %v", name);
}

/*
 * 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 == '=') {
		/* can't have both [+-] and a value */
		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.
 */
struct attribute *
parse_config(entry, error)
    register char *entry;		/* config from read_entry */
    char **error;			/* return error message */
{
    struct str str;			/* area for building result */
    int attr_type = ' ';		/* `+' `-' or SPACE */
    int value_offset;			/* offset in STR(&str) of value */
    struct attribute *attr = (struct attribute *)xmalloc(sizeof(*attr));

    attr->succ = NULL;
    STR_INIT(&str);

#if 0
    DEBUG1(DBG_CONF_HI, "parse_config(): about to process entry:\n\t'%v'\n", entry);
#endif

    /* can be preceded by whitespace */
    entry = skip_space(entry);

    if (*entry == '+') {
	entry++;			/* skip over a leading + */
	attr_type = '+';		/* positive boolean */
    } else if (*entry == '-') {
	entry++;			/* skip over a leading - */
	attr_type = '-';		/* negative boolean */
    }

    /*
     * get the variable name
     */
    while (*entry && (isalnum((int) *entry) || *entry == '_' || *entry == '-')) {
	STR_NEXT(&str, *entry++);
    }
    STR_NEXT(&str, '\0');		/* terminate variable name */

    entry = skip_space(entry);

    if (*entry == '\0') {
	/* boolean variable */
	STR_DONE(&str);
	attr->name = STR(&str);
	if (attr_type == '-') {
	    attr->value = off;
	} else {
	    attr->value = on;
	}

	return attr;
    } else if (*entry != '=') {
#if 0
	DEBUG2(DBG_CONF_LO, "parse_config(): expected `=' after variable name: '%v', in entry\n\t'%v'\n", STR(&str), entry);
#endif
	*error = xprintf("expected `=' after variable name: %v", STR(&str));
	return NULL;
    }

    if (attr_type != ' ') {
#if 0
	DEBUG2(DBG_CONF_LO, "parse_config(): unexpected pattern: `= value' follows boolean variable: '%v', in entry\n\t'%v'\n", STR(&str), entry);
#endif
	*error = xprintf("unexpected pattern:  `= value' follows boolean variable: %v", STR(&str));
	return NULL;
    }

    /* form is name = value, find the value */

    entry = skip_space(entry + 1);

    value_offset = STR_LEN(&str);

    /*
     * XXX this code is very nearly a duplicate of the code above in parse_entry()
     */
    if (*entry == '"') {
#if 0
	DEBUG2(DBG_CONF_HI, "parse_config(): variable `%v': about to slurp quoted value:\n\t'%v'\n", STR(&str), entry);
#endif
	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.
	 */
	/*
	 * XXX this loop is an exact duplicate of the code above in parse_entry()
	 */
	while (*entry && *entry != '"') {
	    if (*entry == '\\') {
		int c;

		entry = c_dequote(entry + 1, &c);
		STR_NEXT(&str, c);
	    } else {
		STR_NEXT(&str, *entry++);
	    }
	}
	if (*entry == '\0') {
	    /*
	     * make sure that the string doesn't suddenly come
	     * to an end at a funny spot
	     */
#if 0
	    DEBUG2(DBG_CONF_LO, "parse_config(): unexpected end of quoted string for variable `%v':\n\t'%v'\n", STR(&str), entry);
#endif
	    *error = xprintf("unexpected end of quoted string for variable %v", STR(&str));
	    return NULL;
	}
	entry++;			/* skip the 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 \ quotes any character.
	 *
	 * Note too that in a config file we allow continuation of variable
	 * values without a trailing backslash.
	 */
#if 0
	DEBUG2(DBG_CONF_HI, "parse_config(): variable %v: about to slurp unquoted value: '%v'\n", STR(&str), entry);
#endif
	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("unexpected end of variable after backslash: %v", STR(&str));
		    return NULL;
		}
	    }
	    STR_NEXT(&str, *entry++);
	}
    }
    STR_NEXT(&str, '\0');		/* close off the value */

    /*
     * make sure this is really the end of the entry, first eating any trailing
     * whitespace that would be ignored anyway....
     */
    entry = skip_space(entry);
    if (*entry != '\0') {
#if 0
	DEBUG2(DBG_CONF_LO, "parse_config(): unexpected data after end of entry for `%v':\n\t'%v'\n", STR(&str), entry);
#endif
	*error = xprintf("unexpected data after end of entry: %v, remaining text: '%v'", STR(&str), entry);
	return NULL;
    }

    STR_DONE(&str);

    attr->name = STR(&str);
    attr->value = STR(&str) + value_offset;

#if 0
    DEBUG2(DBG_CONF_HI, "parse_config(): got var `%v', with value:\n\t'%v'\n", attr->name, attr->value);
#endif

    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 an entry from a file into memory
 *
 * a director, router, transport, or alias file entry 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 we need 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.
 */
char *
read_entry(f)
    register FILE *f;			/* input file */
{
    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) {
			/* XXX we need to pass in the name of the file! */
			panic(EX_OSFILE, "fgetpos(): %s", 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) {
		/* XXX we need to pass in the name of the file! */
		panic(EX_OSFILE, "fsetpos(): %s", 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)) {
	if (file_type == config) {
	    char *error;
	    struct attribute *attr = parse_config(entry, &error);

	    if (attr == NULL) {
		(void) fprintf(stderr, "error in <stdin>: %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 <stdin>: %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 <stdin>: %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:
 */
