Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 784 lines (750 sloc) 18.253 kb
33d9386 initial public release
Peter Seebach authored
1 /*
2 * pseudolog.c, pseudo database viewer (preliminary)
3 *
4 * Copyright (c) 2008-2010 Wind River Systems, Inc.
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the Lesser GNU General Public License version 2.1 as
8 * published by the Free Software Foundation.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13 * See the Lesser GNU General Public License for more details.
14 *
15 * You should have received a copy of the Lesser GNU General Public License
16 * version 2.1 along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 *
19 */
20 /* We need _XOPEN_SOURCE for strptime(), but if we define that,
21 * we then don't get S_IFSOCK... _GNU_SOURCE turns on everything. */
22 #define _GNU_SOURCE
23
24 #include <ctype.h>
25 #include <limits.h>
26 #include <stdio.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <time.h>
30 #include <unistd.h>
31 #include <sys/stat.h>
32
33 #include "pseudo.h"
34 #include "pseudo_ipc.h"
35 #include "pseudo_db.h"
36
37 static int opt_D = 0;
38 static int opt_l = 0;
39
40 static void display(log_entry *, char *format);
41 static unsigned long format_scan(char *format);
42
43 void
44 usage(int status) {
45 static char *options[] = {
46 "c client pid",
47 "d device number",
48 "f file descriptor",
49 "g gid",
50 "G tag (text)",
51 "i inode number",
52 "I id (database row)",
53 "m permission bits (octal)",
54 "M file mode (octal)",
55 "o operation (e.g. 'open')",
56 "O order by (< DESC > ASC)",
57 "p file path",
58 "r result (e.g. 'succeed')",
59 "s timestamp",
60 "S severity",
61 "t type (like find -type)",
62 "T text (text field)",
63 "u uid",
64 NULL,
65 };
66 FILE *f = (status == EXIT_SUCCESS) ? stdout : stderr;
67 int i;
68
69 fputs("pseudolog: create or report log entries. usage:\n", f);
70 fputs("pseudolog -l [-E timeformat] [SPECIFIERS] -- create entries\n", f);
71 fputs("pseudolog [-D] [-F format] [-E timeformat] [SPECIFIERS] -- report entries\n", f);
72 fputs(" format is a printf-like format string using the option letters\n", f);
73 fputs(" listed below as format specifiers for the corresponding field.\n", f);
74 fputs(" timeformat is a strftime-like format string, the default is '%x %X'.\n", f);
75 fputs("\n", f);
76 fputs("SPECIFIERS are options of the form -X <value>, where X is one of\n", f);
77 fputs("the following option letters, and value is the value to match.\n", f);
78 fputs("values may be prefixed with ! (not equal to), > (greater than),\n", f);
79 fputs("< (less than), & (bitwise and), ~ (LIKE match, anchored at both\n", f);
80 fputs("ends, text fields only), or % (LIKE match, text fields only).\n", f);
81 fputs("\n", f);
82 fputs("OPTION LETTERS:\n", f);
83 for (i = 0; options[i]; ++i) {
84 fprintf(f, " %-28s%s", options[i], (i % 2) ? "\n" : " ");
85 }
86 exit(status);
87 }
88
89 pseudo_query_field_t opt_to_field[UCHAR_MAX + 1] = {
90 ['c'] = PSQF_CLIENT,
91 ['d'] = PSQF_DEV,
92 ['f'] = PSQF_FD,
93 ['g'] = PSQF_GID,
94 ['G'] = PSQF_TAG,
95 ['I'] = PSQF_ID,
96 ['i'] = PSQF_INODE,
97 ['m'] = PSQF_PERM,
98 ['M'] = PSQF_MODE,
99 ['o'] = PSQF_OP,
100 ['O'] = PSQF_ORDER,
101 ['p'] = PSQF_PATH,
102 ['r'] = PSQF_RESULT,
103 ['s'] = PSQF_STAMP,
104 ['S'] = PSQF_SEVERITY,
105 ['t'] = PSQF_FTYPE,
106 ['T'] = PSQF_TEXT,
107 ['u'] = PSQF_UID,
108 };
109
110 pseudo_query_type_t
111 plog_query_type(char **string) {
112 pseudo_query_type_t type = PSQT_EXACT;
113 if (!string || !*string)
114 return PSQT_UNKNOWN;
115 switch (**string) {
116 case '\0':
117 pseudo_diag("Error: Value may not be an empty string.");
118 return PSQT_UNKNOWN;
119 break;
120 case '>':
121 type = PSQT_GREATER;
122 ++*string;
123 break;
124 case '<':
125 type = PSQT_LESS;
126 ++*string;
127 break;
128 case '!':
129 type = PSQT_NOTEQUAL;
130 ++*string;
131 break;
132 case '=':
133 ++*string;
134 break;
135 case '&':
136 type = PSQT_BITAND;
137 ++*string;
138 break;
139 case '%':
140 type = PSQT_LIKE;
141 ++*string;
142 break;
143 case '^':
144 type = PSQT_NOTLIKE;
145 ++*string;
146 break;
147 case '~':
148 type = PSQT_SQLPAT;
149 ++*string;
150 break;
151 case '\\':
152 /* no special type, but allows one of the others to be the
153 * first character of the effective string
154 */
155 ++*string;
156 break;
157 }
158 if (opt_l && type != PSQT_EXACT) {
159 pseudo_diag("Error: Non-exact match requested while trying to create a log entry.\n");
160 type = PSQT_UNKNOWN;
161 }
162 return type;
163 }
164
165 static char *time_formats[] = {
166 "%s",
167 "%F %r",
168 "%F %T",
169 "%m-%d %r",
170 "%m-%d %T",
171 "%r",
172 "%T",
173 NULL,
174 };
175 static char *timeformat = "%x %X";
176
177 mode_t
178 parse_file_type(char *string) {
179 switch (*string) {
180 case 'b':
181 return S_IFBLK;
182 break;
183 case 'c':
184 return S_IFCHR;
185 break;
186 case 'd':
187 return S_IFDIR;
188 break;
189 case '-': /* FALLTHROUGH */
190 case 'f':
191 return S_IFREG;
192 break;
193 case 'l':
194 return S_IFLNK;
195 break;
196 case 'p':
197 return S_IFIFO;
198 break;
199 case 's':
200 return S_IFSOCK;
201 break;
202 default:
203 pseudo_diag("unknown file type %c; should be one of [-bcdflps]\n",
204 isprint(*string) ? *string : '?');
205 return -1;
206 break;
207 }
208 }
209
210 mode_t
211 parse_partial_mode(char *string) {
212 mode_t mode = 0;
213 switch (string[0]) {
214 case 'r':
215 mode |= 04;
216 break;
217 case '-':
218 break;
219 default:
220 pseudo_diag("unknown mode character: %c\n", string[0]);
221 return -1;
222 break;
223 }
224 switch (string[1]) {
225 case 'w':
226 mode |= 02;
227 break;
228 case '-':
229 break;
230 default:
231 pseudo_diag("unknown mode character: %c\n", string[1]);
232 return -1;
233 break;
234 }
235 switch (string[2]) {
236 case 'x':
237 mode |= 01;
238 break;
239 case 't': /* FALLTHROUGH */
240 case 's':
241 mode |= 011;
242 break;
243 case 'T': /* FALLTHROUGH */
244 case 'S':
245 mode |= 010;
246 break;
247 case '-':
248 break;
249 default:
250 pseudo_diag("unknown mode character: %c\n", string[2]);
251 return -1;
252 break;
253 }
254 return mode;
255 }
256
257 mode_t
258 parse_mode_string(char *string) {
259 size_t len = strlen(string);
260 mode_t mode = 0;
261 mode_t bits = 0;
262
263 if (len != 9 && len != 10) {
264 pseudo_diag("mode strings must be of the form [-]rwxr-xr-x\n");
265 return -1;
266 }
267 if (len == 10) {
268 mode |= parse_file_type(string);
269 ++string;
270 if (mode == -1) {
271 pseudo_diag("mode strings with a file type must use a valid type [-bcdflps]\n");
272 return -1;
273 }
274 }
275 bits = parse_partial_mode(string);
276 if (bits == -1)
277 return -1;
278 if (bits & 010) {
279 mode |= S_ISUID;
280 bits &= ~010;
281 }
282 mode |= bits << 6;
283 string += 3;
284 bits = parse_partial_mode(string);
285 if (bits == -1)
286 return -1;
287 if (bits & 010) {
288 mode |= S_ISGID;
289 bits &= ~010;
290 }
291 mode |= bits << 3;
292 string += 3;
293 bits = parse_partial_mode(string);
294 if (bits == -1)
295 return -1;
296 if (bits & 010) {
297 mode |= S_ISVTX;
298 bits &= ~010;
299 }
300 mode |= bits;
301 return mode;
302 }
303
304 static time_t
305 parse_timestamp(char *string) {
306 time_t stamp_sec;
307 struct tm stamp_tm;
308 int i;
309 char *s;
310 char timebuf[4096];
311
312 stamp_sec = time(0);
313
314 /* try the user's provided time format first, if there is one: */
315 localtime_r(&stamp_sec, &stamp_tm);
316 s = strptime(string, timeformat, &stamp_tm);
317 if (s && !*s) {
318 return mktime(&stamp_tm);
319 }
320
321 for (i = 0; time_formats[i]; ++i) {
322 char *s;
323 localtime_r(&stamp_sec, &stamp_tm);
324 s = strptime(string, time_formats[i], &stamp_tm);
325 if (s && !*s) {
326 break;
327 }
328 }
329 if (!time_formats[i]) {
330 pseudo_diag("Couldn't parse <%s> as a time. Current time in known formats is:\n",
331 string);
332 localtime_r(&stamp_sec, &stamp_tm);
333 for (i = 0; time_formats[i]; ++i) {
334 strftime(timebuf, sizeof(timebuf), time_formats[i], &stamp_tm);
335 pseudo_diag("\t%s\n", timebuf);
336 }
337 pseudo_diag("Or, specify your own with -E; see strptime(3).\n");
338 return -1;
339 }
340 return mktime(&stamp_tm);
341 }
342
343 pseudo_query_t *
344 plog_trait(int opt, char *string) {
345 pseudo_query_t *new_trait;
346 char *endptr;
347
348 if (opt < 0 || opt > UCHAR_MAX) {
349 pseudo_diag("Unknown/invalid option value: %d\n", opt);
350 return 0;
351 }
352 if (!opt_to_field[opt]) {
353 if (isprint(opt)) {
354 pseudo_diag("Unknown option: -%c\n", opt);
355 } else {
356 pseudo_diag("Unknown option: 0x%02x\n", opt);
357 }
358 return 0;
359 }
360 if (!*string) {
361 pseudo_diag("invalid empty string for -%c\n", opt);
362 return 0;
363 }
364 new_trait = calloc(sizeof(*new_trait), 1);
365 if (!new_trait) {
366 pseudo_diag("Couldn't allocate requested trait (for -%c %s)\n",
367 opt, string ? string : "<nil>");
368 return 0;
369 }
370 new_trait->field = opt_to_field[opt];
371 new_trait->type = plog_query_type(&string);
372 if (new_trait->type == PSQT_UNKNOWN) {
373 pseudo_diag("Couldn't comprehend trait type for '%s'\n",
374 string ? string : "<nil>");
375 free(new_trait);
376 return 0;
377 }
378 switch (new_trait->field) {
379 case PSQF_FTYPE:
380 /* special magic: allow file types ala find */
381 /* This is implemented by additional magic over in the database code */
382 /* must not be more than one character. The test against
383 * the first character is because in theory, if the
384 * first character is the terminating NUL, we may not
385 * access the second. */
386 if (string[0] && string[1]) {
387 pseudo_diag("file type must be a single character [-bcdflps].\n");
388 free(new_trait);
389 return 0;
390 }
391 new_trait->data.ivalue = parse_file_type(string);
392 if (new_trait->data.ivalue == -1) {
393 free(new_trait);
394 return 0;
395 }
396 break;
397 case PSQF_OP:
398 new_trait->data.ivalue = pseudo_op_id(string);
399 break;
400 case PSQF_ORDER:
401 if (string[0] && string[1]) {
402 pseudo_diag("order type must be a single specifier character.\n");
403 free(new_trait);
404 return 0;
405 }
406 new_trait->data.ivalue = opt_to_field[(unsigned char) string[0]];
407 if (!new_trait->data.ivalue) {
408 pseudo_diag("Unknown field type: %c\n", string[0]);
409 }
410 break;
411 case PSQF_RESULT:
412 new_trait->data.ivalue = pseudo_res_id(string);
413 break;
414 case PSQF_SEVERITY:
415 new_trait->data.ivalue = pseudo_sev_id(string);
416 break;
417 case PSQF_STAMP:
418 new_trait->data.ivalue = parse_timestamp(string);
419 if (new_trait->data.ivalue == (time_t) -1) {
420 free(new_trait);
421 return 0;
422 }
423 break;
424 case PSQF_CLIENT:
425 case PSQF_DEV:
426 case PSQF_FD:
427 case PSQF_GID:
428 case PSQF_INODE:
429 case PSQF_UID:
430 new_trait->data.ivalue = strtoll(string, &endptr, 0);
431 if (*endptr) {
432 pseudo_diag("Unexpected garbage after number (%llu): '%s'\n",
433 new_trait->data.ivalue, endptr);
434 free(new_trait);
435 return 0;
436 }
437 break;
438 case PSQF_MODE:
439 case PSQF_PERM:
440 new_trait->data.ivalue = strtoll(string, &endptr, 8);
441 if (!*endptr) {
442 break;
443 }
444 /* maybe it's a mode string? */
445 new_trait->data.ivalue = parse_mode_string(string);
446 if (new_trait->data.ivalue == -1) {
447 free(new_trait);
448 return 0;
449 }
450 if (new_trait->field == PSQF_PERM) {
451 /* mask out file type */
452 new_trait->data.ivalue &= ~S_IFMT;
453 }
454 break;
455 case PSQF_PATH: /* FALLTHROUGH */
456 case PSQF_TEXT: /* FALLTHROUGH */
457 case PSQF_TAG:
458 /* Plain strings */
459 new_trait->data.svalue = strdup(string);
460 break;
461 default:
462 pseudo_diag("I don't know how I got here. Unknown field type %d.\n",
463 new_trait->field);
464 free(new_trait);
465 return 0;
466 break;
467 }
468 return new_trait;
469 }
470
471 /* You can either create a query or create a log entry. They use very
472 * similar syntax, but:
473 * - if you're making a query, you can use >, <, etc.
474 * - if you're logging, you can't.
475 * This is tracked by recording whether any non-exact relations
476 * have been requested ("query_only"), and refusing to set the -l
477 * flag if they have, and refusing to accept any such relation
478 * if the -l flag is already set.
479 */
480 int
481 main(int argc, char **argv) {
482 pseudo_query_t *traits = 0, *current = 0, *new_trait = 0;
483 log_history history;
484 int query_only = 0;
485 int o;
486 int bad_args = 0;
487 char *format = "%s %-5o %7r: [mode %04m] %p %T";
488
489 while ((o = getopt(argc, argv, "vlc:d:DE:f:F:g:G:hi:I:m:M:o:O:p:r:s:S:t:T:u:")) != -1) {
490 switch (o) {
491 case 'P':
492 setenv("PSEUDO_PREFIX", optarg, 1);
493 break;
494 case 'v':
495 pseudo_debug_verbose();
496 break;
497 case 'l':
498 opt_l = 1;
499 break;
500 case 'D':
501 opt_D = 1;
502 query_only = 1;
503 break;
504 case 'E':
505 timeformat = strdup(optarg);
506 break;
507 case 'F':
508 /* disallow specifying -F with -l */
509 format = strdup(optarg);
510 query_only = 1;
511 break;
512 case 'I': /* PSQF_ID */
513 query_only = 1;
514 /* FALLTHROUGH */
515 case 'c': /* PSQF_CLIENT */
516 case 'd': /* PSQF_DEV */
517 case 'f': /* PSQF_FD */
518 case 'g': /* PSQF_GID */
519 case 'G': /* PSQF_TAG */
520 case 'i': /* PSQF_INODE */
521 case 'm': /* PSQF_PERM */
522 case 'M': /* PSQF_MODE */
523 case 'o': /* PSQF_OP */
524 case 'O': /* PSQF_ORDER */
525 case 'p': /* PSQF_PATH */
526 case 'r': /* PSQF_RESULT */
527 case 's': /* PSQF_STAMP */
528 case 'S': /* PSQF_SEVERITY */
529 case 't': /* PSQF_FTYPE */
530 case 'T': /* PSQF_TEXT */
531 case 'u': /* PSQF_UID */
532 new_trait = plog_trait(o, optarg);
533 if (!new_trait) {
534 bad_args = 1;
535 }
536 break;
537 case 'h':
538 usage(EXIT_SUCCESS);
539 break;
540 case '?': /* FALLTHROUGH */
541 default:
542 fprintf(stderr, "unknown option '%c'\n", optopt);
543 usage(EXIT_FAILURE);
544 break;
545 }
546 if (new_trait) {
547 if (current) {
548 current->next = new_trait;
549 current = current->next;
550 } else {
551 traits = new_trait;
552 current = new_trait;
553 }
554 new_trait = 0;
555 }
556 }
557
558 if (optind < argc) {
559 pseudo_diag("Error: Extra arguments not associated with any option.\n");
560 usage(EXIT_FAILURE);
561 }
562
563 if (query_only && opt_l) {
564 pseudo_diag("Error: -l cannot be used with query-only options or flags.\n");
565 bad_args = 1;
566 }
567
568 /* should be set only if we have already diagnosed the bad arguments. */
569 if (bad_args)
570 exit(EXIT_FAILURE);
571
572 if (!pseudo_get_prefix(argv[0])) {
573 pseudo_diag("Can't figure out prefix. Set PSEUDO_PREFIX or invoke with full path.\n");
574 exit(EXIT_FAILURE);
575 }
576
577 if (opt_l) {
578 pdb_log_traits(traits);
579 } else {
580 unsigned long fields;
581 fields = format_scan(format);
582 if (fields == -1) {
583 pseudo_diag("couldn't parse format string (%s).\n", format);
584 return EXIT_FAILURE;
585 }
586 history = pdb_history(traits, fields, opt_D);
587 if (history) {
588 log_entry *e;
589 while ((e = pdb_history_entry(history)) != NULL) {
590 display(e, format);
591 log_entry_free(e);
592 }
593 pdb_history_free(history);
594 } else {
595 pseudo_diag("could not retrieve history.\n");
596 return EXIT_FAILURE;
597 }
598 }
599 return 0;
600 }
601
602 /* print a single member of log, based on a single format specifier;
603 * returns the address of the last character of the format specifier.
604 */
605 static char *
606 format_one(log_entry *e, char *format) {
607 char fmtbuf[256];
608 size_t len = strcspn(format, "cdfgGimMoprsStTu"), real_len;
609 char scratch[4096];
610 time_t stamp_sec;
611 struct tm stamp_tm;
612 char *s;
613
614 if (!e || !format) {
615 pseudo_diag("invalid log entry or format specifier.\n");
616 return 0;
617 }
618 real_len = snprintf(fmtbuf, sizeof(fmtbuf), "%.*s", len + 1, format);
619 if (real_len >= sizeof(fmtbuf) - 1) {
620 pseudo_diag("Format string way too long starting at %.10s",
621 format - 1);
622 return 0;
623 }
624 /* point to the last character */
625 s = fmtbuf + real_len - 1;
626
627 /* The * modifier for width or precision requires additional
628 * parameters -- this doesn't make sense here.
629 */
630 if (strchr(fmtbuf, '*') || strchr(fmtbuf, 'l') || strchr(fmtbuf, 'h')) {
631 pseudo_diag("Sorry, you can't use *, h, or l format modifiers.\n");
632 return 0;
633 }
634
635 switch (*s) {
636 case 'c': /* PSQF_CLIENT */
637 strcpy(s, "d");
638 printf(fmtbuf, (int) e->client);
639 break;
640 case 'd': /* PSQF_DEV */
641 strcpy(s, "d");
642 printf(fmtbuf, (int) e->dev);
643 break;
644 case 'f': /* PSQF_FD */
645 strcpy(s, "d");
646 printf(fmtbuf, (int) e->fd);
647 break;
648 case 'g': /* PSQF_GID */
649 strcpy(s, "d");
650 printf(fmtbuf, (int) e->gid);
651 break;
652 case 'G': /* PSQF_TAG */
653 strcpy(s, "s");
654 printf(fmtbuf, e->tag ? e->tag : "");
655 break;
656 case 'i': /* PSQF_INODE */
657 strcpy(s, "llu");
658 printf(fmtbuf, (unsigned long long) e->ino);
659 break;
660 case 'm': /* PSQF_PERM */
661 strcpy(s, "o");
662 printf(fmtbuf, (unsigned int) e->mode & ALLPERMS);
663 break;
664 case 'M': /* PSQF_MODE */
665 strcpy(s, "o");
666 printf(fmtbuf, (unsigned int) e->mode);
667 break;
668 case 'o': /* PSQF_OP */
669 strcpy(s, "s");
670 printf(fmtbuf, pseudo_op_name(e->op));
671 break;
672 case 'p': /* PSQF_PATH */
673 strcpy(s, "s");
674 printf(fmtbuf, e->path ? e->path : "");
675 break;
676 case 'r': /* PSQF_RESULT */
677 strcpy(s, "s");
678 printf(fmtbuf, pseudo_res_name(e->result));
679 break;
680 case 's': /* PSQF_STAMP */
681 strcpy(s, "s");
682 stamp_sec = e->stamp;
683 localtime_r(&stamp_sec, &stamp_tm);
684 strftime(scratch, sizeof(scratch), timeformat, &stamp_tm);
685 printf(fmtbuf, scratch);
686 break;
687 case 'S': /* PSQF_SEVERITY */
688 strcpy(s, "s");
689 printf(fmtbuf, pseudo_sev_name(e->severity));
690 break;
691 case 't': /* PSQF_FTYPE */
692 strcpy(s, "s");
693 if (S_ISREG(e->mode)) {
694 strcpy(scratch, "file");
695 } if (S_ISLNK(e->mode)) {
696 strcpy(scratch, "link");
697 } else if (S_ISDIR(e->mode)) {
698 strcpy(scratch, "dir");
699 } else if (S_ISFIFO(e->mode)) {
700 strcpy(scratch, "fifo");
701 } else if (S_ISBLK(e->mode)) {
702 strcpy(scratch, "block");
703 } else if (S_ISCHR(e->mode)) {
704 strcpy(scratch, "char");
705 } else {
706 snprintf(scratch, sizeof(scratch), "?%o", (unsigned int) e->mode & S_IFMT);
707 }
708 printf(fmtbuf, scratch);
709 break;
710 case 'T': /* PSQF_TEXT */
711 strcpy(s, "s");
712 printf(fmtbuf, e->text ? e->text : "");
713 break;
714 case 'u': /* PSQF_UID */
715 strcpy(s, "d");
716 printf(fmtbuf, (int) e->uid);
717 break;
718 }
719 return format + len;
720 }
721
722 static unsigned long
723 format_scan(char *format) {
724 char *s;
725 size_t len;
726 unsigned long fields = 0;
727 pseudo_query_field_t field;
728
729 for (s = format; (s = strchr(s, '%')) != NULL; ++s) {
730 len = strcspn(s, "cdfgGimMoprsStTu");
731 s += len;
732 field = opt_to_field[(unsigned char) *s];
733 switch (field) {
734 case PSQF_PERM:
735 case PSQF_FTYPE:
736 fields |= (1 << PSQF_MODE);
737 break;
738 case PSQF_CLIENT:
739 case PSQF_DEV:
740 case PSQF_FD:
741 case PSQF_GID:
742 case PSQF_TAG:
743 case PSQF_INODE:
744 case PSQF_MODE:
745 case PSQF_OP:
746 case PSQF_PATH:
747 case PSQF_RESULT:
748 case PSQF_STAMP:
749 case PSQF_SEVERITY:
750 case PSQF_TEXT:
751 case PSQF_UID:
752 fields |= (1 << field);
753 break;
754 case '\0':
755 /* if there are no more formats, that may be wrong, but
756 * we can ignore it here
757 */
758 break;
759 default:
760 pseudo_diag("error: invalid format specifier %c (at %s)\n", *s, s);
761 return -1;
762 break;
763 }
764 }
765 return fields;
766 }
767
768 static void
769 display(log_entry *e, char *format) {
770 for (; *format; ++format) {
771 switch (*format) {
772 case '%':
773 format = format_one(e, format);
774 if (!format)
775 return;
776 break;
777 default:
778 putchar(*format);
779 break;
780 }
781 }
782 putchar('\n');
783 }
Something went wrong with that request. Please try again.