1 | /* config_read(), _delete(), _length() - Generic config file routines.
|
---|
2 | * Author: Kees J. Bot
|
---|
3 | * 5 Jun 1999
|
---|
4 | */
|
---|
5 | #define nil ((void*)0)
|
---|
6 | #if __minix_vmd
|
---|
7 | #include <minix/stubs.h>
|
---|
8 | #else
|
---|
9 | #define fstat _fstat
|
---|
10 | #define stat _stat
|
---|
11 | #endif
|
---|
12 | #include <sys/types.h>
|
---|
13 | #include <stdio.h>
|
---|
14 | #include <stddef.h>
|
---|
15 | #include <stdlib.h>
|
---|
16 | #include <errno.h>
|
---|
17 | #include <string.h>
|
---|
18 | #include <time.h>
|
---|
19 | #include <sys/stat.h>
|
---|
20 | #if __minix_vmd
|
---|
21 | #include <minix/asciictype.h>
|
---|
22 | #else
|
---|
23 | #include <ctype.h>
|
---|
24 | #endif
|
---|
25 | #define _c /* not const */
|
---|
26 | #include <configfile.h>
|
---|
27 |
|
---|
28 | typedef struct configfile { /* List of (included) configuration files. */
|
---|
29 | struct configfile *next; /* A list indeed. */
|
---|
30 | time_t ctime; /* Last changed time, -1 if no file. */
|
---|
31 | char name[1]; /* File name. */
|
---|
32 | } configfile_t;
|
---|
33 |
|
---|
34 | /* Size of a configfile_t given a file name of length 'len'. */
|
---|
35 | #define configfilesize(len) (offsetof(configfile_t, name) + 1 + (len))
|
---|
36 |
|
---|
37 | typedef struct firstconfig { /* First file and first word share a slot. */
|
---|
38 | configfile_t *filelist;
|
---|
39 | char new; /* Set when created. */
|
---|
40 | config_t config1;
|
---|
41 | } firstconfig_t;
|
---|
42 |
|
---|
43 | /* Size of a config_t given a word of lenght 'len'. Same for firstconfig_t. */
|
---|
44 | #define config0size() (offsetof(config_t, word))
|
---|
45 | #define configsize(len) (config0size() + 1 + (len))
|
---|
46 | #define firstconfigsize(len) \
|
---|
47 | (offsetof(firstconfig_t, config1) + configsize(len))
|
---|
48 |
|
---|
49 | /* Translate address of first config word to enclosing firstconfig_t and vv. */
|
---|
50 | #define cfg2fcfg(p) \
|
---|
51 | ((firstconfig_t *) ((char *) (p) - offsetof(firstconfig_t, config1)))
|
---|
52 | #define fcfg2cfg(p) (&(p)->config1)
|
---|
53 |
|
---|
54 | /* Variables used while building data. */
|
---|
55 | static configfile_t *c_files; /* List of (included) config files. */
|
---|
56 | static int c_flags; /* Flags argument of config_read(). */
|
---|
57 | static FILE *c_fp; /* Current open file. */
|
---|
58 | static char *c_file; /* Current open file name. */
|
---|
59 | static unsigned c_line; /* Current line number. */
|
---|
60 | static int c; /* Next character. */
|
---|
61 |
|
---|
62 | static void *allocate(void *mem, size_t size)
|
---|
63 | /* Like realloc(), but checked. */
|
---|
64 | {
|
---|
65 | if ((mem= realloc(mem, size)) == nil) {
|
---|
66 | fprintf(stderr, "\"%s\", line %u: Out of memory\n", c_file, c_line);
|
---|
67 | exit(1);
|
---|
68 | }
|
---|
69 | return mem;
|
---|
70 | }
|
---|
71 |
|
---|
72 | #define deallocate(mem) free(mem)
|
---|
73 |
|
---|
74 | static void delete_filelist(configfile_t *cfgf)
|
---|
75 | /* Delete configuration file list. */
|
---|
76 | {
|
---|
77 | void *junk;
|
---|
78 |
|
---|
79 | while (cfgf != nil) {
|
---|
80 | junk= cfgf;
|
---|
81 | cfgf= cfgf->next;
|
---|
82 | deallocate(junk);
|
---|
83 | }
|
---|
84 | }
|
---|
85 |
|
---|
86 | static void delete_config(config_t *cfg)
|
---|
87 | /* Delete configuration file data. */
|
---|
88 | {
|
---|
89 | config_t *next, *list, *junk;
|
---|
90 |
|
---|
91 | next= cfg;
|
---|
92 | list= nil;
|
---|
93 | for (;;) {
|
---|
94 | if (next != nil) {
|
---|
95 | /* Push the 'next' chain in reverse on the 'list' chain, putting
|
---|
96 | * a leaf cell (next == nil) on top of 'list'.
|
---|
97 | */
|
---|
98 | junk= next;
|
---|
99 | next= next->next;
|
---|
100 | junk->next= list;
|
---|
101 | list= junk;
|
---|
102 | } else
|
---|
103 | if (list != nil) {
|
---|
104 | /* Delete the leaf cell. If it has a sublist then that becomes
|
---|
105 | * the 'next' chain.
|
---|
106 | */
|
---|
107 | junk= list;
|
---|
108 | next= list->list;
|
---|
109 | list= list->next;
|
---|
110 | deallocate(junk);
|
---|
111 | } else {
|
---|
112 | /* Both chains are gone. */
|
---|
113 | break;
|
---|
114 | }
|
---|
115 | }
|
---|
116 | }
|
---|
117 |
|
---|
118 | void config_delete(config_t *cfg1)
|
---|
119 | /* Delete configuration file data, being careful with the odd first one. */
|
---|
120 | {
|
---|
121 | firstconfig_t *fcfg= cfg2fcfg(cfg1);
|
---|
122 |
|
---|
123 | delete_filelist(fcfg->filelist);
|
---|
124 | delete_config(fcfg->config1.next);
|
---|
125 | delete_config(fcfg->config1.list);
|
---|
126 | deallocate(fcfg);
|
---|
127 | }
|
---|
128 |
|
---|
129 | static void nextc(void)
|
---|
130 | /* Read the next character of the current file into 'c'. */
|
---|
131 | {
|
---|
132 | if (c == '\n') c_line++;
|
---|
133 | c= getc(c_fp);
|
---|
134 | if (c == EOF && ferror(c_fp)) {
|
---|
135 | fprintf(stderr, "\"%s\", line %u: %s\n",
|
---|
136 | c_file, c_line, strerror(errno));
|
---|
137 | exit(1);
|
---|
138 | }
|
---|
139 | }
|
---|
140 |
|
---|
141 | static void skipwhite(void)
|
---|
142 | /* Skip whitespace and comments. */
|
---|
143 | {
|
---|
144 | while (isspace(c)) {
|
---|
145 | nextc();
|
---|
146 | if (c == '#') {
|
---|
147 | do nextc(); while (c != EOF && c != '\n');
|
---|
148 | }
|
---|
149 | }
|
---|
150 | }
|
---|
151 |
|
---|
152 | static void parse_err(void)
|
---|
153 | /* Tell user that you can't parse past the current character. */
|
---|
154 | {
|
---|
155 | char sc[2];
|
---|
156 |
|
---|
157 | sc[0]= c;
|
---|
158 | sc[1]= 0;
|
---|
159 | fprintf(stderr, "\"%s\", line %u: parse error at '%s'\n",
|
---|
160 | c_file, c_line, c == EOF ? "EOF" : sc);
|
---|
161 | exit(1);
|
---|
162 | }
|
---|
163 |
|
---|
164 | static config_t *read_word(void)
|
---|
165 | /* Read a word or string. */
|
---|
166 | {
|
---|
167 | config_t *w;
|
---|
168 | size_t i, len;
|
---|
169 | int q;
|
---|
170 | static char SPECIAL[] = "!#$%&*+-./:<=>?[\\]^_|~";
|
---|
171 |
|
---|
172 | i= 0;
|
---|
173 | len= 32;
|
---|
174 | w= allocate(nil, configsize(32));
|
---|
175 | w->next= nil;
|
---|
176 | w->list= nil;
|
---|
177 | w->file= c_file;
|
---|
178 | w->line= c_line;
|
---|
179 | w->flags= 0;
|
---|
180 |
|
---|
181 | /* Is it a quoted string? */
|
---|
182 | if (c == '\'' || c == '"') {
|
---|
183 | q= c; /* yes */
|
---|
184 | nextc();
|
---|
185 | } else {
|
---|
186 | q= -1; /* no */
|
---|
187 | }
|
---|
188 |
|
---|
189 | for (;;) {
|
---|
190 | if (i == len) {
|
---|
191 | len+= 32;
|
---|
192 | w= allocate(w, configsize(len));
|
---|
193 | }
|
---|
194 |
|
---|
195 | if (q == -1) {
|
---|
196 | /* A word consists of letters, numbers and a few special chars. */
|
---|
197 | if (!isalnum(c) && c < 0x80 && strchr(SPECIAL, c) == nil) break;
|
---|
198 | } else {
|
---|
199 | /* Strings are made up of anything except newlines. */
|
---|
200 | if (c == EOF || c == '\n') {
|
---|
201 | fprintf(stderr,
|
---|
202 | "\"%s\", line %u: string at line %u not closed\n",
|
---|
203 | c_file, c_line, w->line);
|
---|
204 | exit(1);
|
---|
205 | break;
|
---|
206 | }
|
---|
207 | if (c == q) { /* Closing quote? */
|
---|
208 | nextc();
|
---|
209 | break;
|
---|
210 | }
|
---|
211 | }
|
---|
212 |
|
---|
213 | if (c != '\\') { /* Simply add non-escapes. */
|
---|
214 | w->word[i++]= c;
|
---|
215 | nextc();
|
---|
216 | } else { /* Interpret an escape. */
|
---|
217 | nextc();
|
---|
218 | if (isspace(c)) {
|
---|
219 | skipwhite();
|
---|
220 | continue;
|
---|
221 | }
|
---|
222 |
|
---|
223 | if (c_flags & CFG_ESCAPED) {
|
---|
224 | w->word[i++]= '\\'; /* Keep the \ for the caller. */
|
---|
225 | if (i == len) {
|
---|
226 | len+= 32;
|
---|
227 | w= allocate(w, configsize(len));
|
---|
228 | }
|
---|
229 | w->flags |= CFG_ESCAPED;
|
---|
230 | }
|
---|
231 |
|
---|
232 | if (isdigit(c)) { /* Octal escape */
|
---|
233 | int n= 3;
|
---|
234 | int d= 0;
|
---|
235 |
|
---|
236 | do {
|
---|
237 | d= d * 010 + (c - '0');
|
---|
238 | nextc();
|
---|
239 | } while (--n > 0 && isdigit(c));
|
---|
240 | w->word[i++]= d;
|
---|
241 | } else
|
---|
242 | if (c == 'x' || c == 'X') { /* Hex escape */
|
---|
243 | int n= 2;
|
---|
244 | int d= 0;
|
---|
245 |
|
---|
246 | nextc();
|
---|
247 | if (!isxdigit(c)) {
|
---|
248 | fprintf(stderr, "\"%s\", line %u: bad hex escape\n",
|
---|
249 | c_file, c_line);
|
---|
250 | exit(1);
|
---|
251 | }
|
---|
252 | do {
|
---|
253 | d= d * 0x10 + (islower(c) ? (c - 'a' + 0xa) :
|
---|
254 | isupper(c) ? (c - 'A' + 0xA) :
|
---|
255 | (c - '0'));
|
---|
256 | nextc();
|
---|
257 | } while (--n > 0 && isxdigit(c));
|
---|
258 | w->word[i++]= d;
|
---|
259 | } else {
|
---|
260 | switch (c) {
|
---|
261 | case 'a': c= '\a'; break;
|
---|
262 | case 'b': c= '\b'; break;
|
---|
263 | case 'e': c= '\033'; break;
|
---|
264 | case 'f': c= '\f'; break;
|
---|
265 | case 'n': c= '\n'; break;
|
---|
266 | case 'r': c= '\r'; break;
|
---|
267 | case 's': c= ' '; break;
|
---|
268 | case 't': c= '\t'; break;
|
---|
269 | case 'v': c= '\v'; break;
|
---|
270 | default: /* Anything else is kept as-is. */;
|
---|
271 | }
|
---|
272 | w->word[i++]= c;
|
---|
273 | nextc();
|
---|
274 | }
|
---|
275 | }
|
---|
276 | }
|
---|
277 | w->word[i]= 0;
|
---|
278 | if (q != -1) {
|
---|
279 | w->flags |= CFG_STRING;
|
---|
280 | } else {
|
---|
281 | int f;
|
---|
282 | char *end;
|
---|
283 | static char base[]= { 0, 010, 10, 0x10 };
|
---|
284 |
|
---|
285 | if (i == 0) parse_err();
|
---|
286 |
|
---|
287 | /* Can the word be used as a number? */
|
---|
288 | for (f= 0; f < 4; f++) {
|
---|
289 | (void) strtol(w->word, &end, base[f]);
|
---|
290 | if (*end == 0) w->flags |= 1 << (f + 0);
|
---|
291 | (void) strtoul(w->word, &end, base[f]);
|
---|
292 | if (*end == 0) w->flags |= 1 << (f + 4);
|
---|
293 | }
|
---|
294 | }
|
---|
295 | return allocate(w, configsize(i));
|
---|
296 | }
|
---|
297 |
|
---|
298 | static config_t *read_file(const char *file);
|
---|
299 | static config_t *read_list(void);
|
---|
300 |
|
---|
301 | static config_t *read_line(void)
|
---|
302 | /* Read and return one line of the config file. */
|
---|
303 | {
|
---|
304 | config_t *cline, **pcline, *clist;
|
---|
305 |
|
---|
306 | cline= nil;
|
---|
307 | pcline= &cline;
|
---|
308 |
|
---|
309 | for (;;) {
|
---|
310 | skipwhite();
|
---|
311 |
|
---|
312 | if (c == EOF || c == '}') {
|
---|
313 | if(0) if (cline != nil) parse_err();
|
---|
314 | break;
|
---|
315 | } else
|
---|
316 | if (c == ';') {
|
---|
317 | nextc();
|
---|
318 | if (cline != nil) break;
|
---|
319 | } else
|
---|
320 | if (cline != nil && c == '{') {
|
---|
321 | /* A sublist. */
|
---|
322 | nextc();
|
---|
323 | clist= allocate(nil, config0size());
|
---|
324 | clist->next= nil;
|
---|
325 | clist->file= c_file;
|
---|
326 | clist->line= c_line;
|
---|
327 | clist->list= read_list();
|
---|
328 | clist->flags= CFG_SUBLIST;
|
---|
329 | *pcline= clist;
|
---|
330 | pcline= &clist->next;
|
---|
331 | if (c != '}') parse_err();
|
---|
332 | nextc();
|
---|
333 | } else {
|
---|
334 | *pcline= read_word();
|
---|
335 | pcline= &(*pcline)->next;
|
---|
336 | }
|
---|
337 | }
|
---|
338 | return cline;
|
---|
339 | }
|
---|
340 |
|
---|
341 | static config_t *read_list(void)
|
---|
342 | /* Read and return a list of config file commands. */
|
---|
343 | {
|
---|
344 | config_t *clist, **pclist, *cline;
|
---|
345 |
|
---|
346 | clist= nil;
|
---|
347 | pclist= &clist;
|
---|
348 |
|
---|
349 | while ((cline= read_line()) != nil) {
|
---|
350 | if (strcmp(cline->word, "include") == 0) {
|
---|
351 | config_t *file= cline->next;
|
---|
352 | if (file == nil || file->next != nil || !config_isatom(file)) {
|
---|
353 | fprintf(stderr,
|
---|
354 | "\"%s\", line %u: 'include' command requires an argument\n",
|
---|
355 | c_file, cline->line);
|
---|
356 | exit(1);
|
---|
357 | }
|
---|
358 | if (file->flags & CFG_ESCAPED) {
|
---|
359 | char *p, *q;
|
---|
360 | p= q= file->word;
|
---|
361 | for (;;) {
|
---|
362 | if ((*q = *p) == '\\') *q = *++p;
|
---|
363 | if (*q == 0) break;
|
---|
364 | p++;
|
---|
365 | q++;
|
---|
366 | }
|
---|
367 | }
|
---|
368 | file= read_file(file->word);
|
---|
369 | delete_config(cline);
|
---|
370 | *pclist= file;
|
---|
371 | while (*pclist != nil) pclist= &(*pclist)->next;
|
---|
372 | } else {
|
---|
373 | config_t *cfg= allocate(nil, config0size());
|
---|
374 | cfg->next= nil;
|
---|
375 | cfg->list= cline;
|
---|
376 | cfg->file= cline->file;
|
---|
377 | cfg->line= cline->line;
|
---|
378 | cfg->flags= CFG_SUBLIST;
|
---|
379 | *pclist= cfg;
|
---|
380 | pclist= &cfg->next;
|
---|
381 | }
|
---|
382 | }
|
---|
383 | return clist;
|
---|
384 | }
|
---|
385 |
|
---|
386 | static config_t *read_file(const char *file)
|
---|
387 | /* Read and return a configuration file. */
|
---|
388 | {
|
---|
389 | configfile_t *cfgf;
|
---|
390 | config_t *cfg;
|
---|
391 | struct stat st;
|
---|
392 | FILE *old_fp; /* old_* variables store current file context. */
|
---|
393 | char *old_file;
|
---|
394 | unsigned old_line;
|
---|
395 | int old_c;
|
---|
396 | size_t n;
|
---|
397 | char *slash;
|
---|
398 |
|
---|
399 | old_fp= c_fp;
|
---|
400 | old_file= c_file;
|
---|
401 | old_line= c_line;
|
---|
402 | old_c= c;
|
---|
403 |
|
---|
404 | n= 0;
|
---|
405 | if (file[0] != '/' && old_file != nil
|
---|
406 | && (slash= strrchr(old_file, '/')) != nil) {
|
---|
407 | n= slash - old_file + 1;
|
---|
408 | }
|
---|
409 | cfgf= allocate(nil, configfilesize(n + strlen(file)));
|
---|
410 | memcpy(cfgf->name, old_file, n);
|
---|
411 | strcpy(cfgf->name + n, file);
|
---|
412 | cfgf->next= c_files;
|
---|
413 | c_files= cfgf;
|
---|
414 |
|
---|
415 | c_file= cfgf->name;
|
---|
416 | c_line= 0;
|
---|
417 |
|
---|
418 | if ((c_fp= fopen(file, "r")) == nil || fstat(fileno(c_fp), &st) < 0) {
|
---|
419 | if (errno != ENOENT) {
|
---|
420 | fprintf(stderr, "\"%s\", line 1: %s\n", file, strerror(errno));
|
---|
421 | exit(1);
|
---|
422 | }
|
---|
423 | cfgf->ctime= -1;
|
---|
424 | c= EOF;
|
---|
425 | } else {
|
---|
426 | cfgf->ctime= st.st_ctime;
|
---|
427 | c= '\n';
|
---|
428 | }
|
---|
429 |
|
---|
430 | cfg= read_list();
|
---|
431 | if (c != EOF) parse_err();
|
---|
432 |
|
---|
433 | if (c_fp != nil) fclose(c_fp);
|
---|
434 | c_fp= old_fp;
|
---|
435 | c_file= old_file;
|
---|
436 | c_line= old_line;
|
---|
437 | c= old_c;
|
---|
438 | return cfg;
|
---|
439 | }
|
---|
440 |
|
---|
441 | config_t *config_read(const char *file, int flags, config_t *cfg)
|
---|
442 | /* Read and parse a configuration file. */
|
---|
443 | {
|
---|
444 | if (cfg != nil) {
|
---|
445 | /* First check if any of the involved files has changed. */
|
---|
446 | firstconfig_t *fcfg;
|
---|
447 | configfile_t *cfgf;
|
---|
448 | struct stat st;
|
---|
449 |
|
---|
450 | fcfg= cfg2fcfg(cfg);
|
---|
451 | for (cfgf= fcfg->filelist; cfgf != nil; cfgf= cfgf->next) {
|
---|
452 | if (stat(cfgf->name, &st) < 0) {
|
---|
453 | if (errno != ENOENT) break;
|
---|
454 | st.st_ctime= -1;
|
---|
455 | }
|
---|
456 | if (st.st_ctime != cfgf->ctime) break;
|
---|
457 | }
|
---|
458 |
|
---|
459 | if (cfgf == nil) return cfg; /* Everything as it was. */
|
---|
460 | config_delete(cfg); /* Otherwise delete and reread. */
|
---|
461 | }
|
---|
462 |
|
---|
463 | errno= 0;
|
---|
464 | c_files= nil;
|
---|
465 | c_flags= flags;
|
---|
466 | cfg= read_file(file);
|
---|
467 |
|
---|
468 | if (cfg != nil) {
|
---|
469 | /* Change first word to have a hidden pointer to a file list. */
|
---|
470 | size_t len= strlen(cfg->word);
|
---|
471 | firstconfig_t *fcfg;
|
---|
472 |
|
---|
473 | fcfg= allocate(cfg, firstconfigsize(len));
|
---|
474 | memmove(&fcfg->config1, fcfg, configsize(len));
|
---|
475 | fcfg->filelist= c_files;
|
---|
476 | fcfg->new= 1;
|
---|
477 | return fcfg2cfg(fcfg);
|
---|
478 | }
|
---|
479 | /* Couldn't read (errno != 0) of nothing read (errno == 0). */
|
---|
480 | delete_filelist(c_files);
|
---|
481 | delete_config(cfg);
|
---|
482 | return nil;
|
---|
483 | }
|
---|
484 |
|
---|
485 | int config_renewed(config_t *cfg)
|
---|
486 | {
|
---|
487 | int new;
|
---|
488 |
|
---|
489 | if (cfg == nil) {
|
---|
490 | new= 1;
|
---|
491 | } else {
|
---|
492 | new= cfg2fcfg(cfg)->new;
|
---|
493 | cfg2fcfg(cfg)->new= 0;
|
---|
494 | }
|
---|
495 | return new;
|
---|
496 | }
|
---|
497 |
|
---|
498 | size_t config_length(config_t *cfg)
|
---|
499 | /* Count the number of items on a list. */
|
---|
500 | {
|
---|
501 | size_t n= 0;
|
---|
502 |
|
---|
503 | while (cfg != nil) {
|
---|
504 | n++;
|
---|
505 | cfg= cfg->next;
|
---|
506 | }
|
---|
507 | return n;
|
---|
508 | }
|
---|
509 |
|
---|
510 | #if TEST
|
---|
511 | #include <unistd.h>
|
---|
512 |
|
---|
513 | static void print_list(int indent, config_t *cfg);
|
---|
514 |
|
---|
515 | static void print_words(int indent, config_t *cfg)
|
---|
516 | {
|
---|
517 | while (cfg != nil) {
|
---|
518 | if (config_isatom(cfg)) {
|
---|
519 | if (config_isstring(cfg)) fputc('"', stdout);
|
---|
520 | printf("%s", cfg->word);
|
---|
521 | if (config_isstring(cfg)) fputc('"', stdout);
|
---|
522 | } else {
|
---|
523 | printf("{\n");
|
---|
524 | print_list(indent+4, cfg->list);
|
---|
525 | printf("%*s}", indent, "");
|
---|
526 | }
|
---|
527 | cfg= cfg->next;
|
---|
528 | if (cfg != nil) fputc(' ', stdout);
|
---|
529 | }
|
---|
530 | printf(";\n");
|
---|
531 | }
|
---|
532 |
|
---|
533 | static void print_list(int indent, config_t *cfg)
|
---|
534 | {
|
---|
535 | while (cfg != nil) {
|
---|
536 | if (!config_issub(cfg)) {
|
---|
537 | fprintf(stderr, "Cell at \"%s\", line %u is not a sublist\n");
|
---|
538 | break;
|
---|
539 | }
|
---|
540 | printf("%*s", indent, "");
|
---|
541 | print_words(indent, cfg->list);
|
---|
542 | cfg= cfg->next;
|
---|
543 | }
|
---|
544 | }
|
---|
545 |
|
---|
546 | static void print_config(config_t *cfg)
|
---|
547 | {
|
---|
548 | if (!config_renewed(cfg)) {
|
---|
549 | printf("# Config didn't change\n");
|
---|
550 | } else {
|
---|
551 | print_list(0, cfg);
|
---|
552 | }
|
---|
553 | }
|
---|
554 |
|
---|
555 | int main(int argc, char **argv)
|
---|
556 | {
|
---|
557 | config_t *cfg;
|
---|
558 | int c;
|
---|
559 |
|
---|
560 | if (argc != 2) {
|
---|
561 | fprintf(stderr, "One config file name please\n");
|
---|
562 | exit(1);
|
---|
563 | }
|
---|
564 |
|
---|
565 | cfg= nil;
|
---|
566 | do {
|
---|
567 | cfg= config_read(argv[1], CFG_ESCAPED, cfg);
|
---|
568 | print_config(cfg);
|
---|
569 | if (!isatty(0)) break;
|
---|
570 | while ((c= getchar()) != EOF && c != '\n') {}
|
---|
571 | } while (c != EOF);
|
---|
572 | return 0;
|
---|
573 | }
|
---|
574 | #endif /* TEST */
|
---|