xref: /illumos-gate/usr/src/cmd/logadm/conf.c (revision 6e6545bfaed3bab9ce836ee82d1abd8f2edba89a)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9  * or http://www.opensolaris.org/os/licensing.
10  * See the License for the specific language governing permissions
11  * and limitations under the License.
12  *
13  * When distributing Covered Code, include this CDDL HEADER in each
14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15  * If applicable, add the following below this CDDL HEADER, with the
16  * fields enclosed by brackets "[]" replaced with your own identifying
17  * information: Portions Copyright [yyyy] [name of copyright owner]
18  *
19  * CDDL HEADER END
20  */
21 
22 /*
23  * Copyright (c) 2001, 2010, Oracle and/or its affiliates. All rights reserved.
24  * Copyright 2013, Joyent, Inc. All rights reserved.
25  * Copyright 2018 Sebastian Wiedenroth
26  */
27 
28 /*
29  * logadm/conf.c -- configuration file module
30  */
31 
32 #include <stdio.h>
33 #include <libintl.h>
34 #include <fcntl.h>
35 #include <sys/types.h>
36 #include <sys/stat.h>
37 #include <sys/mman.h>
38 #include <ctype.h>
39 #include <strings.h>
40 #include <unistd.h>
41 #include <stdlib.h>
42 #include <limits.h>
43 #include "err.h"
44 #include "lut.h"
45 #include "fn.h"
46 #include "opts.h"
47 #include "conf.h"
48 
49 /* forward declarations of functions private to this module */
50 static void fillconflist(int lineno, const char *entry,
51     struct opts *opts, const char *com, int flags);
52 static void fillargs(char *arg);
53 static char *nexttok(char **ptrptr);
54 static void conf_print(FILE *cstream, FILE *tstream);
55 
56 static const char *Confname;	/* name of the confile file */
57 static int Conffd = -1;		/* file descriptor for config file */
58 static char *Confbuf;		/* copy of the config file (a la mmap()) */
59 static int Conflen;		/* length of mmap'd config file area */
60 static const char *Timesname;	/* name of the timestamps file */
61 static int Timesfd = -1;	/* file descriptor for timestamps file */
62 static char *Timesbuf;		/* copy of the timestamps file (a la mmap()) */
63 static int Timeslen;		/* length of mmap'd timestamps area */
64 static int Singlefile;		/* Conf and Times in the same file */
65 static int Changed;		/* what changes need to be written back */
66 static int Canchange;		/* what changes can be written back */
67 static int Changing;		/* what changes have been requested */
68 #define	CHG_NONE	0
69 #define	CHG_TIMES	1
70 #define	CHG_BOTH	3
71 
72 /*
73  * our structured representation of the configuration file
74  * is made up of a list of these
75  */
76 struct confinfo {
77 	struct confinfo *cf_next;
78 	int cf_lineno;		/* line number in file */
79 	const char *cf_entry;	/* name of entry, if line has an entry */
80 	struct opts *cf_opts;	/* parsed rhs of entry */
81 	const char *cf_com;	/* any comment text found */
82 	int cf_flags;
83 };
84 
85 #define	CONFF_DELETED	1	/* entry should be deleted on write back */
86 #define	CONFF_TSONLY	2	/* entry should only be in timestamps file */
87 
88 static struct confinfo *Confinfo;	/* the entries in the config file */
89 static struct confinfo *Confinfolast;	/* end of list */
90 static struct lut *Conflut;		/* lookup table keyed by entry name */
91 static struct fn_list *Confentries;	/* list of valid entry names */
92 
93 /* allocate & fill in another entry in our list */
94 static void
95 fillconflist(int lineno, const char *entry,
96     struct opts *opts, const char *com, int flags)
97 {
98 	struct confinfo *cp = MALLOC(sizeof (*cp));
99 
100 	cp->cf_next = NULL;
101 	cp->cf_lineno = lineno;
102 	cp->cf_entry = entry;
103 	cp->cf_opts = opts;
104 	cp->cf_com = com;
105 	cp->cf_flags = flags;
106 	if (entry != NULL) {
107 		Conflut = lut_add(Conflut, entry, cp);
108 		fn_list_adds(Confentries, entry);
109 	}
110 	if (Confinfo == NULL)
111 		Confinfo = Confinfolast = cp;
112 	else {
113 		Confinfolast->cf_next = cp;
114 		Confinfolast = cp;
115 	}
116 }
117 
118 static char **Args;	/* static buffer for args */
119 static int ArgsN;	/* size of our static buffer */
120 static int ArgsI;	/* index into Cmdargs as we walk table */
121 #define	CONF_ARGS_INC	1024
122 
123 /* callback for lut_walk to build a cmdargs vector */
124 static void
125 fillargs(char *arg)
126 {
127 	if (ArgsI >= ArgsN) {
128 		/* need bigger table */
129 		Args = REALLOC(Args, sizeof (char *) * (ArgsN + CONF_ARGS_INC));
130 		ArgsN += CONF_ARGS_INC;
131 	}
132 	Args[ArgsI++] = arg;
133 }
134 
135 /* isolate and return the next token */
136 static char *
137 nexttok(char **ptrptr)
138 {
139 	char *ptr = *ptrptr;
140 	char *eptr;
141 	char *quote = NULL;
142 
143 	while (*ptr && isspace(*ptr))
144 		ptr++;
145 
146 	if (*ptr == '"' || *ptr == '\'')
147 		quote = ptr++;
148 
149 	for (eptr = ptr; *eptr; eptr++)
150 		if (quote && *eptr == *quote) {
151 			/* found end quote */
152 			*eptr++ = '\0';
153 			*ptrptr = eptr;
154 			return (ptr);
155 		} else if (!quote && isspace(*eptr)) {
156 			/* found end of unquoted area */
157 			*eptr++ = '\0';
158 			*ptrptr = eptr;
159 			return (ptr);
160 		}
161 
162 	if (quote != NULL)
163 		err(EF_FILE|EF_JMP, "Unbalanced %c quote", *quote);
164 		/*NOTREACHED*/
165 
166 	*ptrptr = eptr;
167 
168 	if (ptr == eptr)
169 		return (NULL);
170 	else
171 		return (ptr);
172 }
173 
174 /*
175  * scan the memory image of a file
176  *	returns: 0: error, 1: ok, 3: -P option found
177  */
178 static int
179 conf_scan(const char *fname, char *buf, int buflen, int timescan)
180 {
181 	int ret = 1;
182 	int lineno = 0;
183 	int flags = 0;
184 	char *line;
185 	char *eline;
186 	char *ebuf;
187 	char *entry, *comment;
188 
189 	ebuf = &buf[buflen];
190 
191 	if (buf[buflen - 1] != '\n')
192 		err(EF_WARN|EF_FILE, "file %s doesn't end with newline, "
193 		    "last line ignored.", fname);
194 
195 	for (line = buf; line < ebuf; line = eline) {
196 		char *ap;
197 		struct opts *opts = NULL;
198 		struct confinfo *cp;
199 
200 		lineno++;
201 		err_fileline(fname, lineno);
202 		eline = line;
203 		comment = NULL;
204 		for (; eline < ebuf; eline++) {
205 			/* check for continued lines */
206 			if (comment == NULL && *eline == '\\' &&
207 			    eline + 1 < ebuf && *(eline + 1) == '\n') {
208 				*eline = ' ';
209 				*(eline + 1) = ' ';
210 				lineno++;
211 				err_fileline(fname, lineno);
212 				continue;
213 			}
214 
215 			/* check for comments */
216 			if (comment == NULL && *eline == '#') {
217 				*eline = '\0';
218 				comment = (eline + 1);
219 				continue;
220 			}
221 
222 			/* check for end of line */
223 			if (*eline == '\n')
224 				break;
225 		}
226 		if (comment >= ebuf)
227 			comment = NULL;
228 		if (eline >= ebuf) {
229 			/* discard trailing unterminated line */
230 			continue;
231 		}
232 		*eline++ = '\0';
233 
234 		/*
235 		 * now we have the entry, if any, at "line"
236 		 * and the comment, if any, at "comment"
237 		 */
238 
239 		/* entry is first token */
240 		entry = nexttok(&line);
241 		if (entry == NULL) {
242 			/* it's just a comment line */
243 			if (!timescan)
244 				fillconflist(lineno, entry, NULL, comment, 0);
245 			continue;
246 		}
247 		if (strcmp(entry, "logadm-version") == 0) {
248 			/*
249 			 * we somehow opened some future format
250 			 * conffile that we likely don't understand.
251 			 * if the given version is "1" then go on,
252 			 * otherwise someone is mixing versions
253 			 * and we can't help them other than to
254 			 * print an error and exit.
255 			 */
256 			if ((entry = nexttok(&line)) != NULL &&
257 			    strcmp(entry, "1") != 0)
258 				err(0, "%s version not supported "
259 				    "by this version of logadm.",
260 				    fname);
261 			continue;
262 		}
263 
264 		/* form an argv array */
265 		ArgsI = 0;
266 		while (ap = nexttok(&line))
267 			fillargs(ap);
268 
269 		/*
270 		 * If there is no next token on the line, make sure that
271 		 * we get a non-NULL Args array.
272 		 */
273 		if (Args == NULL)
274 			fillargs(NULL);
275 
276 		Args[ArgsI] = NULL;
277 
278 		LOCAL_ERR_BEGIN {
279 			if (SETJMP) {
280 				err(EF_FILE, "cannot process invalid entry %s",
281 				    entry);
282 				ret = 0;
283 				LOCAL_ERR_BREAK;
284 			}
285 
286 			if (timescan) {
287 				/* append to config options */
288 				cp = lut_lookup(Conflut, entry);
289 				if (cp != NULL) {
290 					opts = cp->cf_opts;
291 				}
292 			}
293 			opts = opts_parse(opts, Args, OPTF_CONF);
294 			if (!timescan || cp == NULL) {
295 				/*
296 				 * If we're not doing timescan, we track this
297 				 * entry.  If we are doing timescan and have
298 				 * what looks like an orphaned entry (cp ==
299 				 * NULL) then we also have to track. See the
300 				 * comment in rotatelog. We need to allow for
301 				 * the case where the logname is not the same as
302 				 * the log file name.
303 				 */
304 				flags = 0;
305 				if (cp == NULL)
306 					flags = CONFF_TSONLY;
307 				fillconflist(lineno, entry, opts, comment,
308 				    flags);
309 			}
310 		LOCAL_ERR_END }
311 
312 		if (ret == 1 && opts && opts_optarg(opts, "P") != NULL)
313 			ret = 3;
314 	}
315 
316 	err_fileline(NULL, 0);
317 	return (ret);
318 }
319 
320 /*
321  * conf_open -- open the configuration file, lock it if we have write perms
322  */
323 int
324 conf_open(const char *cfname, const char *tfname, struct opts *cliopts)
325 {
326 	struct stat stbuf1, stbuf2, stbuf3;
327 	struct flock	flock;
328 	int ret;
329 
330 	Confname = cfname;
331 	Timesname = tfname;
332 	Confentries = fn_list_new(NULL);
333 	Changed = CHG_NONE;
334 
335 	Changing = CHG_TIMES;
336 	if (opts_count(cliopts, "Vn") != 0)
337 		Changing = CHG_NONE;
338 	else if (opts_count(cliopts, "rw") != 0)
339 		Changing = CHG_BOTH;
340 
341 	Singlefile = strcmp(Confname, Timesname) == 0;
342 	if (Singlefile && Changing == CHG_TIMES)
343 		Changing = CHG_BOTH;
344 
345 	/* special case this so we don't even try locking the file */
346 	if (strcmp(Confname, "/dev/null") == 0)
347 		return (0);
348 
349 	while (Conffd == -1) {
350 		Canchange = CHG_BOTH;
351 		if ((Conffd = open(Confname, O_RDWR)) < 0) {
352 			if (Changing == CHG_BOTH)
353 				err(EF_SYS, "open %s", Confname);
354 			Canchange = CHG_TIMES;
355 			if ((Conffd = open(Confname, O_RDONLY)) < 0)
356 				err(EF_SYS, "open %s", Confname);
357 		}
358 
359 		flock.l_type = (Canchange == CHG_BOTH) ? F_WRLCK : F_RDLCK;
360 		flock.l_whence = SEEK_SET;
361 		flock.l_start = 0;
362 		flock.l_len = 1;
363 		if (fcntl(Conffd, F_SETLKW, &flock) < 0)
364 			err(EF_SYS, "flock on %s", Confname);
365 
366 		/* wait until after file is locked to get filesize */
367 		if (fstat(Conffd, &stbuf1) < 0)
368 			err(EF_SYS, "fstat on %s", Confname);
369 
370 		/* verify that we've got a lock on the active file */
371 		if (stat(Confname, &stbuf2) < 0 ||
372 		    !(stbuf2.st_dev == stbuf1.st_dev &&
373 		    stbuf2.st_ino == stbuf1.st_ino)) {
374 			/* wrong config file, try again */
375 			(void) close(Conffd);
376 			Conffd = -1;
377 		}
378 	}
379 
380 	while (!Singlefile && Timesfd == -1) {
381 		if ((Timesfd = open(Timesname, O_CREAT|O_RDWR, 0644)) < 0) {
382 			if (Changing != CHG_NONE)
383 				err(EF_SYS, "open %s", Timesname);
384 			Canchange = CHG_NONE;
385 			if ((Timesfd = open(Timesname, O_RDONLY)) < 0)
386 				err(EF_SYS, "open %s", Timesname);
387 		}
388 
389 		flock.l_type = (Canchange != CHG_NONE) ? F_WRLCK : F_RDLCK;
390 		flock.l_whence = SEEK_SET;
391 		flock.l_start = 0;
392 		flock.l_len = 1;
393 		if (fcntl(Timesfd, F_SETLKW, &flock) < 0)
394 			err(EF_SYS, "flock on %s", Timesname);
395 
396 		/* wait until after file is locked to get filesize */
397 		if (fstat(Timesfd, &stbuf2) < 0)
398 			err(EF_SYS, "fstat on %s", Timesname);
399 
400 		/* verify that we've got a lock on the active file */
401 		if (stat(Timesname, &stbuf3) < 0 ||
402 		    !(stbuf2.st_dev == stbuf3.st_dev &&
403 		    stbuf2.st_ino == stbuf3.st_ino)) {
404 			/* wrong timestamp file, try again */
405 			(void) close(Timesfd);
406 			Timesfd = -1;
407 			continue;
408 		}
409 
410 		/* check that Timesname isn't an alias for Confname */
411 		if (stbuf2.st_dev == stbuf1.st_dev &&
412 		    stbuf2.st_ino == stbuf1.st_ino)
413 			err(0, "Timestamp file %s can't refer to "
414 			    "Configuration file %s", Timesname, Confname);
415 	}
416 
417 	Conflen = stbuf1.st_size;
418 	Timeslen = stbuf2.st_size;
419 
420 	if (Conflen == 0)
421 		return (1);	/* empty file, don't bother parsing it */
422 
423 	if ((Confbuf = (char *)mmap(0, Conflen,
424 	    PROT_READ | PROT_WRITE, MAP_PRIVATE, Conffd, 0)) == (char *)-1)
425 		err(EF_SYS, "mmap on %s", Confname);
426 
427 	ret = conf_scan(Confname, Confbuf, Conflen, 0);
428 	if (ret == 3 && !Singlefile && Canchange == CHG_BOTH) {
429 		/*
430 		 * arrange to transfer any timestamps
431 		 * from conf_file to timestamps_file
432 		 */
433 		Changing = Changed = CHG_BOTH;
434 	}
435 
436 	if (Timesfd != -1 && Timeslen != 0) {
437 		if ((Timesbuf = (char *)mmap(0, Timeslen,
438 		    PROT_READ | PROT_WRITE, MAP_PRIVATE,
439 		    Timesfd, 0)) == (char *)-1)
440 			err(EF_SYS, "mmap on %s", Timesname);
441 		ret &= conf_scan(Timesname, Timesbuf, Timeslen, 1);
442 	}
443 
444 	/*
445 	 * possible future enhancement:  go through and mark any entries:
446 	 * 		logfile -P <date>
447 	 * as DELETED if the logfile doesn't exist
448 	 */
449 
450 	return (ret);
451 }
452 
453 /*
454  * conf_close -- close the configuration file
455  */
456 void
457 conf_close(struct opts *opts)
458 {
459 	char cuname[PATH_MAX], tuname[PATH_MAX];
460 	int cfd, tfd;
461 	FILE *cfp = NULL, *tfp = NULL;
462 	boolean_t safe_update = B_TRUE;
463 
464 	if (Changed == CHG_NONE || opts_count(opts, "n") != 0) {
465 		if (opts_count(opts, "v"))
466 			(void) out("# %s and %s unchanged\n",
467 			    Confname, Timesname);
468 		goto cleanup;
469 	}
470 
471 	if (Debug > 1) {
472 		(void) fprintf(stderr, "conf_close, saving logadm context:\n");
473 		conf_print(stderr, NULL);
474 	}
475 
476 	cuname[0] = tuname[0] = '\0';
477 	LOCAL_ERR_BEGIN {
478 		if (SETJMP) {
479 			safe_update = B_FALSE;
480 			LOCAL_ERR_BREAK;
481 		}
482 		if (Changed == CHG_BOTH) {
483 			if (Canchange != CHG_BOTH)
484 				err(EF_JMP, "internal error: attempting "
485 				    "to update %s without locking", Confname);
486 			(void) snprintf(cuname, sizeof (cuname), "%sXXXXXX",
487 			    Confname);
488 			if ((cfd = mkstemp(cuname)) == -1)
489 				err(EF_SYS|EF_JMP, "open %s replacement",
490 				    Confname);
491 			if (opts_count(opts, "v"))
492 				(void) out("# writing changes to %s\n", cuname);
493 			if (fchmod(cfd, 0644) == -1)
494 				err(EF_SYS|EF_JMP, "chmod %s", cuname);
495 			if ((cfp = fdopen(cfd, "w")) == NULL)
496 				err(EF_SYS|EF_JMP, "fdopen on %s", cuname);
497 		} else {
498 			/* just toss away the configuration data */
499 			cfp = fopen("/dev/null", "w");
500 		}
501 		if (!Singlefile) {
502 			if (Canchange == CHG_NONE)
503 				err(EF_JMP, "internal error: attempting "
504 				    "to update %s without locking", Timesname);
505 			(void) snprintf(tuname, sizeof (tuname), "%sXXXXXX",
506 			    Timesname);
507 			if ((tfd = mkstemp(tuname)) == -1)
508 				err(EF_SYS|EF_JMP, "open %s replacement",
509 				    Timesname);
510 			if (opts_count(opts, "v"))
511 				(void) out("# writing changes to %s\n", tuname);
512 			if (fchmod(tfd, 0644) == -1)
513 				err(EF_SYS|EF_JMP, "chmod %s", tuname);
514 			if ((tfp = fdopen(tfd, "w")) == NULL)
515 				err(EF_SYS|EF_JMP, "fdopen on %s", tuname);
516 		}
517 
518 		conf_print(cfp, tfp);
519 		if (fclose(cfp) < 0)
520 			err(EF_SYS|EF_JMP, "fclose on %s", Confname);
521 		if (tfp != NULL && fclose(tfp) < 0)
522 			err(EF_SYS|EF_JMP, "fclose on %s", Timesname);
523 	LOCAL_ERR_END }
524 
525 	if (!safe_update) {
526 		if (cuname[0] != 0)
527 			(void) unlink(cuname);
528 		if (tuname[0] != 0)
529 			(void) unlink(tuname);
530 		err(EF_JMP, "unsafe to update configuration file "
531 		    "or timestamps");
532 		return;
533 	}
534 
535 	/* rename updated files into place */
536 	if (cuname[0] != '\0')
537 		if (rename(cuname, Confname) < 0)
538 			err(EF_SYS, "rename %s to %s", cuname, Confname);
539 	if (tuname[0] != '\0')
540 		if (rename(tuname, Timesname) < 0)
541 			err(EF_SYS, "rename %s to %s", tuname, Timesname);
542 	Changed = CHG_NONE;
543 
544 cleanup:
545 	if (Conffd != -1) {
546 		(void) close(Conffd);
547 		Conffd = -1;
548 	}
549 	if (Timesfd != -1) {
550 		(void) close(Timesfd);
551 		Timesfd = -1;
552 	}
553 	if (Conflut) {
554 		lut_free(Conflut, free);
555 		Conflut = NULL;
556 	}
557 	if (Confentries) {
558 		fn_list_free(Confentries);
559 		Confentries = NULL;
560 	}
561 }
562 
563 /*
564  * conf_lookup -- lookup an entry in the config file
565  */
566 void *
567 conf_lookup(const char *lhs)
568 {
569 	struct confinfo *cp = lut_lookup(Conflut, lhs);
570 
571 	if (cp != NULL)
572 		err_fileline(Confname, cp->cf_lineno);
573 	return (cp);
574 }
575 
576 /*
577  * conf_opts -- return the parsed opts for an entry
578  */
579 struct opts *
580 conf_opts(const char *lhs)
581 {
582 	struct confinfo *cp = lut_lookup(Conflut, lhs);
583 
584 	if (cp != NULL)
585 		return (cp->cf_opts);
586 	return (opts_parse(NULL, NULL, OPTF_CONF));
587 }
588 
589 /*
590  * conf_replace -- replace an entry in the config file
591  */
592 void
593 conf_replace(const char *lhs, struct opts *newopts)
594 {
595 	struct confinfo *cp = lut_lookup(Conflut, lhs);
596 
597 	if (Conffd == -1)
598 		return;
599 
600 	if (cp != NULL) {
601 		cp->cf_opts = newopts;
602 		/* cp->cf_args = NULL; */
603 		if (newopts == NULL)
604 			cp->cf_flags |= CONFF_DELETED;
605 	} else
606 		fillconflist(0, lhs, newopts, NULL, 0);
607 
608 	Changed = CHG_BOTH;
609 }
610 
611 /*
612  * conf_set -- set options for an entry in the config file
613  */
614 void
615 conf_set(const char *entry, char *o, const char *optarg)
616 {
617 	struct confinfo *cp = lut_lookup(Conflut, entry);
618 
619 	if (Conffd == -1)
620 		return;
621 
622 	if (cp != NULL) {
623 		cp->cf_flags &= ~CONFF_DELETED;
624 	} else {
625 		fillconflist(0, STRDUP(entry),
626 		    opts_parse(NULL, NULL, OPTF_CONF), NULL, 0);
627 		if ((cp = lut_lookup(Conflut, entry)) == NULL)
628 			err(0, "conf_set internal error");
629 	}
630 	(void) opts_set(cp->cf_opts, o, optarg);
631 	if (strcmp(o, "P") == 0)
632 		Changed |= CHG_TIMES;
633 	else
634 		Changed = CHG_BOTH;
635 }
636 
637 /*
638  * conf_entries -- list all the entry names
639  */
640 struct fn_list *
641 conf_entries(void)
642 {
643 	return (Confentries);
644 }
645 
646 /* print the config file */
647 static void
648 conf_print(FILE *cstream, FILE *tstream)
649 {
650 	struct confinfo *cp;
651 	char *exclude_opts = "PFfhnrvVw";
652 	const char *timestamp;
653 
654 	if (tstream == NULL) {
655 		exclude_opts++;		/* -P option goes to config file */
656 	} else {
657 		(void) fprintf(tstream, gettext(
658 		    "# This file holds internal data for logadm(1M).\n"
659 		    "# Do not edit.\n"));
660 	}
661 	for (cp = Confinfo; cp; cp = cp->cf_next) {
662 		if (cp->cf_flags & CONFF_DELETED)
663 			continue;
664 		if (cp->cf_entry) {
665 			/* output timestamps to tstream */
666 			if (tstream != NULL && (timestamp =
667 			    opts_optarg(cp->cf_opts, "P")) != NULL) {
668 				opts_printword(cp->cf_entry, tstream);
669 				(void) fprintf(tstream, " -P ");
670 				opts_printword(timestamp, tstream);
671 				(void) fprintf(tstream, "\n");
672 			}
673 			if (cp->cf_flags & CONFF_TSONLY)
674 				continue;
675 
676 			opts_printword(cp->cf_entry, cstream);
677 			if (cp->cf_opts)
678 				opts_print(cp->cf_opts, cstream, exclude_opts);
679 		}
680 		if (cp->cf_com) {
681 			if (cp->cf_entry)
682 				(void) fprintf(cstream, " ");
683 			(void) fprintf(cstream, "#%s", cp->cf_com);
684 		}
685 		(void) fprintf(cstream, "\n");
686 	}
687 }
688 
689 #ifdef	TESTMODULE
690 
691 /*
692  * test main for conf module, usage: a.out conffile
693  */
694 int
695 main(int argc, char *argv[])
696 {
697 	struct opts *opts;
698 
699 	err_init(argv[0]);
700 	setbuf(stdout, NULL);
701 	opts_init(Opttable, Opttable_cnt);
702 
703 	opts = opts_parse(NULL, NULL, 0);
704 
705 	if (argc != 2)
706 		err(EF_RAW, "usage: %s conffile\n", argv[0]);
707 
708 	conf_open(argv[1], argv[1], opts);
709 
710 	printf("conffile <%s>:\n", argv[1]);
711 	conf_print(stdout, NULL);
712 
713 	conf_close(opts);
714 
715 	err_done(0);
716 	/* NOTREACHED */
717 	return (0);
718 }
719 
720 #endif	/* TESTMODULE */
721