14e5cc4af74be6560055a5c8479f4abe1a02ec7c
[ccan] / junkcode / tinkertim@gmail.com-grawk / grawk.c
1 /* Copyright (c) 2008, Tim Post <tinkertim@gmail.com>
2  * All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are met:
6  *
7  * Redistributions of source code must retain the above copyright notice, this
8  * list of conditions and the following disclaimer.
9  *
10  * Redistributions in binary form must reproduce the above copyright notice,
11  * this list of conditions and the following disclaimer in the documentation
12  * and/or other materials provided with the distribution.
13  *
14  * Neither the name of the original program's authors nor the names of its
15  * contributors may be used to endorse or promote products derived from this
16  * software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
22  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28  * POSSIBILITY OF SUCH DAMAGE.
29  */
30  
31 /* Some example usages:
32  * grawk shutdown '$5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15' messages
33  * grawk shutdown '$5, $6, $7, $8, $9, $10, " -- " $1, $2, $3' messages
34  * grawk dhclient '$1, $2 " \"$$\"-- " $3' syslog
35  * cat syslog | grawk dhclient '$0'
36  * cat myservice.log | grawk -F , error '$3'
37  *
38  * Contributors:
39  * Tim Post, Nicholas Clements, Alex Karlov
40  * We hope that you find this useful! */
41
42 /* FIXME:
43  * readline() should probably be renamed
44  */
45
46 /* TODO:
47  * Add a tail -f like behavior that applies expressions and fields
48  * Recursive (like grep -r) or at least honor symlinks ? */
49
50 #include <stdio.h>
51 #include <stdlib.h>
52 #include <string.h>
53 #include <getopt.h>
54 #include <sys/types.h>
55 #include <sys/stat.h>
56 #include <regex.h>
57
58 #define VERSION    "1.0.7"
59 #define MAINTAINER "Tim Post <echo@echoreply.us>"
60
61 /* Storage structure to hold awk-style pattern */
62 struct awk_pattern
63 {
64         int maxfield;   /* Maximum field number for $# fields */
65         int numfields;  /* Number of awk pattern fields */
66         char **fields;  /* The awk pattern fields */
67 };
68
69 typedef struct awk_pattern awk_pat_t;
70
71 /* Option arguments */
72 static struct option const long_options[] = {
73         { "ignore-case",     no_argument,       0, 'i' },
74         { "with-filename",   no_argument,       0, 'W' },
75         { "no-filename",     no_argument,       0, 'w' },
76         { "line-number",     no_argument,       0, 'n' },
77         { "field-separator", required_argument, 0, 'F' },
78         { "help",            no_argument,       0, 'h' },
79         { "version",         no_argument,       0, 'v' },
80         { 0, 0, 0, 0}
81 };
82
83 /* The official name of the program */
84 const char *progname = "grawk";
85
86 /* Global for delimiters used in tokenizing strings */
87 char *tokdelim = NULL;
88
89 /* Prototypes */
90 static void usage(void);
91 static int process(FILE *, regex_t, awk_pat_t, char *, int);
92 static int process_line(char *, awk_pat_t, char *, char *);
93 static int process_files(int, char **, regex_t, awk_pat_t, int, int);
94 static int process_pipe(regex_t, awk_pat_t, int);
95 static int awkcomp(awk_pat_t *, char *);
96 static void awkfree(awk_pat_t *);
97 static char *readline(FILE *);
98
99 static void usage(void)
100 {
101         printf("%s %s\n", progname, VERSION);
102         printf("Usage: %s [OPTION] PATTERN OUTPUT_PATTERN file1 [file2]...\n",
103                         progname);
104         printf("Options:\n");
105         printf("  --help                       "
106                 "show help and examples\n");
107         printf("  -i, --ignore-case            "
108                 "ignore case distinctions\n");
109         printf("  -W, --with-filename          "
110                 "Print filename for each match\n");
111         printf("  -w, --no-filename            "
112                 "Never print filename for each match\n");
113         printf("  -n, --line-number            "
114                 "Prefix each line of output with line number.\n");
115         printf("  -F fs, --field-separator=fs  "
116                 "Use fs as the field separator\n");
117         printf("  -h, --help                   "
118                 "Print a brief help summary\n");
119         printf("  -v, --version                "
120                 "Print version information and exit normally\n");
121         printf("  PATTERN                      "
122                 "a basic regular expression\n");
123         printf("  OUTPUT_PATTERN               "
124                 "awk-style print statement; defines "
125                 "output fields\n");
126         printf("\nExamples:\n");
127         printf("  Retreive joe123's home directory from /etc/passwd:\n");
128         printf("\t%s -F : \"joe123\" '$6' /etc/passwd\n", progname);
129         printf("\n  Find fields 2 3 and 4 on lines that begin with @ from stdin:\n");
130         printf("\tcat file.txt | %s \"^@\" '$2,$3,$4'\n", progname);
131         printf("\n  Use as a simple grep:\n");
132         printf("\t%s \"string to find\" '$0' /file.txt\n", progname);
133         printf("\nReport bugs to %s\n", MAINTAINER);
134 }
135
136 /* readline() - read a line from the file handle.
137  * Return an allocated string */
138 static char *readline(FILE *fp)
139 {
140         char *str = (char *)NULL;
141         int ch = 0, len = 256, step = 256, i = 0;
142
143         str = (char *)malloc(len);
144         if (str == NULL)
145                 return str;
146
147         while (1) {
148                 ch = fgetc(fp);
149                 if (feof(fp))
150                         break;
151                 if (ch == '\n' || ch == '\r') {
152                         str[i++] = 0;
153                         break;
154                 }
155                 str[i++] = ch;
156                 if (i == len - 2) {
157                         len += step;
158                         str = (char *)realloc(str, len);
159                         if (str == NULL) {
160                                 fclose(fp);
161                                 return str;
162                         }
163                 }
164         }
165         return str;
166 }
167
168 /* process() - this is the actual processing where we compare against a
169  * previously compiled grep pattern and output based on the awk pattern.
170  * The file is opened by the calling function. We pass in an empty string
171  * if we don't want to show the filename. If we want to show the line number,
172  * the value of show_lineno is 1. If we find a line, return 1. If no line is
173  * found, return 0. If an error occurs, return -1. */
174 static int process(FILE *fp, regex_t re, awk_pat_t awk,
175         char *filename, int show_lineno)
176 {
177         char *inbuf = NULL;
178         char slineno[32];
179         memset(slineno, 0, sizeof(slineno));
180         long lineno = 0;
181         int found = 0;
182
183         while (1) {
184                 inbuf = readline(fp);
185                 if (!inbuf)
186                         break;
187                 if (feof(fp))
188                         break;
189                 lineno++;
190                 if (regexec(&re, inbuf, (size_t)0, NULL, 0) == 0) {
191                         found = 1;  // Found a line.
192                         if (show_lineno)
193                                 sprintf(slineno, "%ld:", lineno);
194                         if (process_line(inbuf, awk, filename, slineno)) {
195                                 fprintf (stderr, "Error processing line [%s]\n", inbuf);
196                                 free (inbuf);
197                                 return -1;
198                         }
199                 }
200                 free (inbuf);
201         }
202
203         if (inbuf)
204                 free(inbuf);
205
206         return found;
207 }
208
209 /* process_files() - process one or more files from the command-line.
210  * If at least one line is found, return 1, else return 0 if no lines
211  * were found or an error occurs. */
212 static int process_files(int numfiles, char **files, regex_t re, awk_pat_t awk,
213                 int show_filename, int show_lineno)
214 {
215         int i, found = 0;
216         FILE *fp = NULL;
217         struct stat fstat;
218         char filename[1024];
219         memset(filename, 0, sizeof(filename));
220
221         for(i = 0; i < numfiles; i++) {
222                 if (stat(files[i], &fstat) == -1) {
223                         /* Did a file get deleted from the time we started running? */
224                         fprintf (stderr,
225                                 "Error accessing file %s. No such file\n", files[i]);
226                         continue;
227                 }
228                 if (show_filename)
229                         sprintf( filename, "%s:", files[i] );
230                 /* For now, we aren't recursive. Perhaps allow symlinks? */
231                 if ((fstat.st_mode & S_IFMT) != S_IFREG)
232                         continue;
233                 if (NULL == (fp = fopen(files[i], "r"))) {
234                         fprintf(stderr,
235                                 "Error opening file %s. Permission denied\n", files[i]);
236                         continue;
237                 }
238                 if (process(fp, re, awk, filename, show_lineno) == 1)
239                         found = 1;
240                 fclose(fp);
241         }
242
243         return found;
244 }
245
246 /* process_pipe() - process input from stdin */
247 static int process_pipe(regex_t re, awk_pat_t awk, int show_lineno)
248 {
249         if (process(stdin, re, awk, "", show_lineno) == 1)
250                 return 1;
251
252         return 0;
253 }
254
255 /* process_line() - process the line based on the awk-style pattern and output
256  * the results. */
257 static int process_line(char *inbuf, awk_pat_t awk, char *filename, char *lineno)
258 {
259         char full_line[3] = { '\1', '0', '\0' };
260
261         if (awk.numfields == 1 && strcmp(awk.fields[0], full_line) == 0) {
262                 /* If the caller only wants the whole string, oblige, quickly. */
263                 fprintf (stdout, "%s%s%s\n", filename, lineno, inbuf);
264                 return 0;
265         }
266
267         /* Build an array of fields from the line using strtok()
268          * TODO: make this re-entrant so that grawk can be spawned as a thread */
269         char **linefields = (char **)malloc((awk.maxfield + 1) * sizeof(char *));
270         char *wrkbuf = strdup(inbuf), *tbuf;
271
272         int count = 0, n = 1, i;
273         for (i = 0; i < (awk.maxfield + 1); i++) {
274                 linefields[i] = NULL;
275         }
276
277         tbuf = strtok(wrkbuf, tokdelim);
278         if(tbuf)
279                 linefields[0] = strdup(tbuf);
280
281         while (tbuf != NULL) {
282                 tbuf = strtok(NULL, tokdelim);
283                 if (!tbuf)
284                         break;
285                 count++;
286                 if (count > awk.maxfield)
287                         break;
288                 linefields[count] = strdup(tbuf);
289                 if (!linefields[count]) {
290                         fprintf(stderr, "Could not allocate memory to process file %s\n",
291                                 filename);
292                         return -1;
293                 }
294         }
295         /* For each field in the awk structure,
296          * find the field and print it to stdout.*/
297         fprintf(stdout, "%s%s", filename, lineno);      /* if needed */
298         for (i = 0; i < awk.numfields; i++) {
299                 if (awk.fields[i][0] == '\1') {
300                         n = atoi(&awk.fields[i][1]);
301                         if (n == 0) {
302                                 fprintf(stdout, "%s", inbuf);
303                                 continue;
304                         }
305                         if (linefields[n-1])
306                                 fprintf(stdout, "%s", linefields[n-1]);
307                         continue;
308                 } else
309                         fprintf(stdout, "%s", awk.fields[i]);
310         }
311         fprintf(stdout, "\n");
312         /* Cleanup */
313         if (wrkbuf)
314                 free(wrkbuf);
315
316         for (i = 0; i < count; i++) {
317                 free(linefields[i]);
318                 linefields[i] = (char *) NULL;
319         }
320
321         free(linefields);
322         linefields = (char **)NULL;
323
324         return 0;
325 }
326
327 /* awkcomp() - little awk-style print format compilation routine.
328  * Returns structure with the apattern broken down into an array for easier
329  * comparison and printing. Handles string literals as well as fields and
330  * delimiters. Example: $1,$2 " \$ and \"blah\" " $4
331  * Returns -1 on error, else 0. */
332 static int awkcomp(awk_pat_t *awk, char *apattern)
333 {
334         awk->maxfield = 0;
335         awk->numfields = 0;
336         awk->fields = NULL;
337         awk->fields = (char **)malloc(sizeof(char *));
338
339         int i, num = 0;
340         char *wrkbuf;
341
342         wrkbuf = (char *)malloc(strlen(apattern) + 1);
343         if (wrkbuf == NULL) {
344                 free(awk);
345                 fprintf(stderr, "Memory allocation error (wrkbuf) in awkcomp()\n");
346                 return -1;
347         }
348
349         int inString = 0, offs = 0;
350         char ch;
351         for (i = 0; i < strlen( apattern ); i++) {
352                 ch = apattern[i];
353                 if (inString && ch != '"' && ch != '\\') {
354                         wrkbuf[offs++] = ch;
355                         continue;
356                 }
357                 if (ch == ' ')
358                         continue;
359                 switch (ch) {
360                 /* Handle delimited strings inside of literal strings */
361                 case '\\':
362                         if (inString) {
363                                 wrkbuf[offs++] = apattern[++i];
364                                 continue;
365                         } else {
366                                 /* Unexpected and unconventional escape (can get these
367                                  * from improper invocations of sed in a pipe with grawk),
368                                  * if sed is used to build the field delimiters */
369                                 fprintf(stderr,
370                                         "Unexpected character \'\\\' in output format\n");
371                                 return -1;
372                         }
373                         break;
374                 /* Beginning or ending of a literal string */
375                 case '"':
376                         inString = !inString;
377                         if (inString)
378                                 continue;
379                         break;
380                 /* Handle the awk-like $# field variables */
381                 case '$':
382                         /* We use a non-printable ASCII character to
383                          * delimit the string field values.*/
384                         wrkbuf[offs++] = '\1';
385                         /* We also need the max. field number */
386                         num = 0;
387                         while (1) {
388                                 ch = apattern[++i];
389                                 /* Not a number, exit this loop */
390                                 if (ch < 48 || ch > 57) {
391                                         i--;
392                                         break;
393                                 }
394                                 num = (num * 10) + (ch - 48);
395                                 wrkbuf[offs++] = ch;
396                         }
397                         if (num > awk->maxfield)
398                                 awk->maxfield = num;
399                         /* Incomplete expression, a $ not followed by a number */
400                         if (wrkbuf[1] == 0) {
401                                 fprintf(stderr, "Incomplete field descriptor at "
402                                                 "or near character %d in awk pattern\n", i+1);
403                                 return -1;
404                         }
405                         break;
406                 /* Field separator */
407                 case ',':
408                         wrkbuf[offs++] = ' ';
409                         break;
410                 }
411                 /* if wrkbuf has nothing, we've got rubbish. Continue in the hopes
412                  * that something else makes sense. */
413                 if (offs == 0)
414                         continue;
415                 /* End of a field reached, put it into awk->fields */
416                 wrkbuf[offs] = '\0';
417                 awk->fields =
418                         (char **)realloc(awk->fields, (awk->numfields + 1)
419                                 * sizeof(char *));
420                 if (!awk->fields ) {
421                         fprintf(stderr,
422                                 "Memory allocation error (awk->fields) in awkcomp()\n");
423                         return -1;
424                 }
425                 awk->fields[awk->numfields] = strdup(wrkbuf);
426                 if (!awk->fields[awk->numfields]) {
427                         fprintf(stderr,
428                                 "Memory allocation error (awk->fields[%d]) in awkcomp()\n",
429                                         awk->numfields);
430                         return -1;
431                 }
432                 memset(wrkbuf, 0, strlen(apattern) + 1);
433                 awk->numfields++;
434                 offs = 0;
435         }
436
437         free(wrkbuf);
438
439         if (awk->numfields == 0) {
440                 fprintf(stderr,
441                         "Unable to parse and compile the pattern; no fields found\n");
442                 return -1;
443         }
444
445         return 0;
446 }
447
448 /* awkfree() - free a previously allocated awk_pat structure */
449 static void awkfree(awk_pat_t *awk )
450 {
451         int i;
452         for (i = 0; i < awk->numfields; i++)
453                 free(awk->fields[i]);
454
455         free(awk->fields);
456 }
457
458 int main(int argc, char **argv)
459 {
460         char *apattern = NULL, *gpattern = NULL;
461         char **files = NULL;
462         int numfiles = 0, i = 0, c = 0;
463         int ignore_case = 0, no_filename = 0, with_filename = 0, line_number = 0;
464
465         if (argc < 3) {
466                 usage();
467                 return EXIT_FAILURE;
468         }
469
470         tokdelim = strdup("\t\r\n ");
471         while (1) {
472                 int opt_ind = 0;
473                 while (c != -1) {
474                         c = getopt_long(argc, argv, "wWhinF:", long_options, &opt_ind);
475                         switch (c) {
476                         case 'w':
477                                 with_filename = 0;
478                                 no_filename = 1;
479                                 break;
480                         case 'i':
481                                 ignore_case = 1;
482                                 break;
483                         case 'W':
484                                 with_filename = 1;
485                                 no_filename = 0;
486                                 break;
487                         case 'n':
488                                 line_number = 1;
489                                 break;
490                         case 'F':
491                                 tokdelim = realloc(tokdelim, 3 + strlen(optarg) + 1);
492                                 memset(tokdelim, 0, 3 + strlen( optarg ) + 1);
493                                 sprintf(tokdelim, "\t\r\n%s", optarg);
494                                 break;
495                         case 'h':
496                                 usage();
497                                 free(tokdelim);
498                                 return EXIT_SUCCESS;
499                                 break;
500                         case 'v':
501                                 printf("%s\n", VERSION);
502                                 free(tokdelim);
503                                 return EXIT_SUCCESS;
504                                 break;
505                         }
506                 }
507
508                 /* Now we'll grab our patterns and files. */
509                 if ((argc - optind) < 2) {
510                         usage();
511                         free(tokdelim);
512                         return EXIT_FAILURE;
513                 }
514
515                 /* pattern one will be our "grep" pattern */
516                 gpattern = strdup(argv[optind]);
517                 if (gpattern == NULL) {
518                         fprintf(stderr, "Memory allocation error");
519                         exit(EXIT_FAILURE);
520                 }
521                 optind++;
522
523                 /* pattern two is our "awk" pattern */
524                 apattern = strdup(argv[optind]);
525                 if(apattern == NULL) {
526                         fprintf(stderr, "Memory allocation error");
527                         exit(EXIT_FAILURE);
528                 }
529                 optind++;
530
531                 /* Anything that remains is a file or wildcard which should be
532                  * expanded by the calling shell. */
533                 if (optind < argc) {
534                         numfiles = argc - optind;
535                         files = (char **)malloc(sizeof(char *) * (numfiles + 1));
536                         for (i = 0; i < numfiles; i++) {
537                                 files[i] = strdup(argv[optind + i]);
538                         }
539                 }
540                 /* If the number of files is greater than 1 then we default to
541                  * showing the filename unless specifically directed against it.*/
542                 if (numfiles > 1 && no_filename == 0)
543                         with_filename = 1;
544                 break;
545         }
546
547         /* Process everything */
548         regex_t re;
549         int cflags = 0, rc = 0;
550
551         if (ignore_case)
552                 cflags = REG_ICASE;
553         /* compile the regular expression parser */
554         if (regcomp(&re, gpattern, cflags)) {
555                 fprintf(stderr,
556                         "Error compiling grep-style pattern [%s]\n", gpattern);
557                 return EXIT_FAILURE;
558         }
559
560         awk_pat_t awk;
561         if (awkcomp(&awk, apattern))
562         {
563                 fprintf(stderr,
564                         "Error compiling awk-style pattern [%s]\n", apattern);
565                 return EXIT_FAILURE;
566         }
567
568         if (numfiles > 0) {
569                 if(process_files(
570                         numfiles, files, re, awk, with_filename, line_number) == 0)
571                         rc = 255;   // We'll return 255 if no lines were found.
572         } else {
573                 if(process_pipe(re, awk, line_number) == 0)
574                         rc = 255;
575         }
576
577         /* Destructor */
578         for (i = 0; i < numfiles; i++) {
579                 if (files[i])
580                         free(files[i]);
581         }
582         free(files);
583
584         /* Awk pattern */
585         free(apattern);
586
587         /* Grep pattern */
588         free(gpattern);
589
590         /* Grep regex */
591         regfree(&re);
592
593         /* Awk pattern structure */
594         awkfree(&awk);
595
596         /* Token delimiter (might have been freed elsewhere) */
597         if (tokdelim)
598                 free(tokdelim);
599         return rc;
600 }