master xplshn/aruu / cmd / posix / join.c
  1/* See LICENSE file for copyright and license details. */
  2
  3#include <ctype.h>
  4#include <stdint.h>
  5#include <stdio.h>
  6#include <stdlib.h>
  7#include <string.h>
  8
  9#include "text.h"
 10#include "utf.h"
 11#include "util.h"
 12
 13enum {
 14  INIT = 1,
 15  GROW = 2,
 16};
 17
 18enum {
 19  EXPAND = 0,
 20  RESET  = 1,
 21};
 22
 23enum {
 24  FIELD_ERROR = -2,
 25};
 26
 27struct field {
 28  char  *s;
 29  size_t len;
 30};
 31
 32struct jline {
 33  struct line   text;
 34  size_t        nf;
 35  size_t        maxf;
 36  struct field *fields;
 37};
 38
 39struct spec {
 40  size_t fileno;
 41  size_t fldno;
 42};
 43
 44struct outlist {
 45  size_t        ns;
 46  size_t        maxs;
 47  struct spec **specs;
 48};
 49
 50struct span {
 51  size_t         nl;
 52  size_t         maxl;
 53  struct jline **lines;
 54};
 55
 56static char          *sep        = NULL;
 57static char          *replace    = NULL;
 58static const char     defaultofs = ' ';
 59static const int      jfield     = 1; /* POSIX default join field */
 60static int            unpairsa = 0, unpairsb = 0;
 61static int            oflag = 0;
 62static int            pairs = 1;
 63static size_t         seplen;
 64static struct outlist output;
 65
 66static void
 67usage(void)
 68{
 69  eprintf(
 70      "usage: %s [-1 field] [-2 field] [-o list] [-e string] "
 71      "[-a | -v fileno] [-t delim] file1 file2\n",
 72      argv0
 73  );
 74}
 75
 76static void
 77prfield(struct field *fp)
 78{
 79  if (fwrite(fp->s, 1, fp->len, stdout) != fp->len)
 80    eprintf("fwrite:");
 81}
 82
 83static void
 84prsep(void)
 85{
 86  if (sep)
 87    fwrite(sep, 1, seplen, stdout);
 88  else
 89    putchar(defaultofs);
 90}
 91
 92static void
 93swaplines(struct jline *la, struct jline *lb)
 94{
 95  struct jline tmp;
 96
 97  tmp = *la;
 98  *la = *lb;
 99  *lb = tmp;
100}
101
102static void
103prjoin(struct jline *la, struct jline *lb, size_t jfa, size_t jfb)
104{
105  struct spec  *sp;
106  struct field *joinfield;
107  size_t        i;
108
109  if (jfa >= la->nf || jfb >= lb->nf)
110    return;
111
112  joinfield = &la->fields[jfa];
113
114  if (oflag) {
115    for (i = 0; i < output.ns; i++) {
116      sp = output.specs[i];
117
118      if (sp->fileno == 1) {
119        if (sp->fldno < la->nf)
120          prfield(&la->fields[sp->fldno]);
121        else if (replace)
122          fputs(replace, stdout);
123      } else if (sp->fileno == 2) {
124        if (sp->fldno < lb->nf)
125          prfield(&lb->fields[sp->fldno]);
126        else if (replace)
127          fputs(replace, stdout);
128      } else if (sp->fileno == 0) {
129        prfield(joinfield);
130      }
131
132      if (i < output.ns - 1)
133        prsep();
134    }
135  } else {
136    prfield(joinfield);
137    prsep();
138
139    for (i = 0; i < la->nf; i++) {
140      if (i != jfa) {
141        prfield(&la->fields[i]);
142        prsep();
143      }
144    }
145    for (i = 0; i < lb->nf; i++) {
146      if (i != jfb) {
147        prfield(&lb->fields[i]);
148        if (i < lb->nf - 1)
149          prsep();
150      }
151    }
152  }
153  putchar('\n');
154}
155
156static void
157prline(struct jline *lp)
158{
159  if (fwrite(lp->text.data, 1, lp->text.len, stdout) != lp->text.len)
160    eprintf("fwrite:");
161  putchar('\n');
162}
163
164static int
165jlinecmp(struct jline *la, struct jline *lb, size_t jfa, size_t jfb)
166{
167  int status;
168
169  /* return FIELD_ERROR if both lines are short */
170  if (jfa >= la->nf) {
171    status = (jfb >= lb->nf) ? FIELD_ERROR : -1;
172  } else if (jfb >= lb->nf) {
173    status = 1;
174  } else {
175    status =
176        memcmp(la->fields[jfa].s, lb->fields[jfb].s, MAX(la->fields[jfa].len, lb->fields[jfb].len));
177    LIMIT(status, -1, 1);
178  }
179
180  return status;
181}
182
183static void
184addfield(struct jline *lp, char *sp, size_t len)
185{
186  if (lp->nf >= lp->maxf) {
187    lp->fields = ereallocarray(lp->fields, (GROW * lp->maxf), sizeof(struct field));
188    lp->maxf *= GROW;
189  }
190  lp->fields[lp->nf].s   = sp;
191  lp->fields[lp->nf].len = len;
192  lp->nf++;
193}
194
195static void
196prspanjoin(struct span *spa, struct span *spb, size_t jfa, size_t jfb)
197{
198  size_t i, j;
199
200  for (i = 0; i < (spa->nl - 1); i++)
201    for (j = 0; j < (spb->nl - 1); j++)
202      prjoin(spa->lines[i], spb->lines[j], jfa, jfb);
203}
204
205static struct jline *
206makeline(char *s, size_t len)
207{
208  struct jline *lp;
209  char         *tmp;
210  size_t        i, end;
211
212  if (s[len - 1] == '\n')
213    s[--len] = '\0';
214
215  lp            = ereallocarray(NULL, INIT, sizeof(struct jline));
216  lp->text.data = s;
217  lp->text.len  = len;
218  lp->fields    = ereallocarray(NULL, INIT, sizeof(struct field));
219  lp->nf        = 0;
220  lp->maxf      = INIT;
221
222  for (i = 0; i < lp->text.len && isblank(lp->text.data[i]); i++)
223    ;
224  while (i < lp->text.len) {
225    if (sep) {
226      if ((lp->text.len - i) < seplen
227          || !(tmp = memmem(lp->text.data + i, lp->text.len - i, sep, seplen))) {
228        goto eol;
229      }
230      end = tmp - lp->text.data;
231      addfield(lp, lp->text.data + i, end - i);
232      i = end + seplen;
233    } else {
234      for (end = i; !(isblank(lp->text.data[end])); end++) {
235        if (end + 1 == lp->text.len)
236          goto eol;
237      }
238      addfield(lp, lp->text.data + i, end - i);
239      for (i = end; isblank(lp->text.data[i]); i++)
240        ;
241    }
242  }
243eol:
244  addfield(lp, lp->text.data + i, lp->text.len - i);
245
246  return lp;
247}
248
249static int
250addtospan(struct span *sp, FILE *fp, int reset)
251{
252  char   *newl = NULL;
253  ssize_t len;
254  size_t  size = 0;
255
256  if ((len = getline(&newl, &size, fp)) < 0) {
257    if (ferror(fp))
258      eprintf("getline:");
259    else
260      return 0;
261  }
262
263  if (reset)
264    sp->nl = 0;
265
266  if (sp->nl >= sp->maxl) {
267    sp->lines = ereallocarray(sp->lines, (GROW * sp->maxl), sizeof(struct jline *));
268    sp->maxl *= GROW;
269  }
270
271  sp->lines[sp->nl] = makeline(newl, len);
272  sp->nl++;
273  return 1;
274}
275
276static void
277initspan(struct span *sp)
278{
279  sp->nl    = 0;
280  sp->maxl  = INIT;
281  sp->lines = ereallocarray(NULL, INIT, sizeof(struct jline *));
282}
283
284static void
285freespan(struct span *sp)
286{
287  size_t i;
288
289  for (i = 0; i < sp->nl; i++) {
290    free(sp->lines[i]->fields);
291    free(sp->lines[i]->text.data);
292  }
293  free(sp->lines);
294}
295
296static void
297initolist(struct outlist *olp)
298{
299  olp->ns    = 0;
300  olp->maxs  = 1;
301  olp->specs = ereallocarray(NULL, INIT, sizeof(struct spec *));
302}
303
304static void
305addspec(struct outlist *olp, struct spec *sp)
306{
307  if (olp->ns >= olp->maxs) {
308    olp->specs = ereallocarray(olp->specs, (GROW * olp->maxs), sizeof(struct spec *));
309    olp->maxs *= GROW;
310  }
311  olp->specs[olp->ns] = sp;
312  olp->ns++;
313}
314
315static struct spec *
316makespec(char *s)
317{
318  struct spec *sp;
319  int          fileno;
320  size_t       fldno;
321
322  if (!strcmp(s, "0")) { /* join field must be 0 and nothing else */
323    fileno = 0;
324    fldno  = 0;
325  } else if ((s[0] == '1' || s[0] == '2') && s[1] == '.') {
326    fileno = s[0] - '0';
327    fldno =
328        estrtonum(&s[2], 1, MIN((unsigned long long)LLONG_MAX, (unsigned long long)SIZE_MAX)) - 1;
329  } else {
330    eprintf("%s: invalid format\n", s);
331  }
332
333  sp         = ereallocarray(NULL, INIT, sizeof(struct spec));
334  sp->fileno = fileno;
335  sp->fldno  = fldno;
336  return sp;
337}
338
339static void
340makeolist(struct outlist *olp, char *s)
341{
342  char *item, *sp;
343  sp = s;
344
345  while (sp) {
346    item = sp;
347    sp   = strpbrk(sp, ", \t");
348    if (sp)
349      *sp++ = '\0';
350    addspec(olp, makespec(item));
351  }
352}
353
354static void
355freespecs(struct outlist *olp)
356{
357  size_t i;
358
359  for (i = 0; i < olp->ns; i++)
360    free(olp->specs[i]);
361}
362
363static void
364join(FILE *fa, FILE *fb, size_t jfa, size_t jfb)
365{
366  struct span spa, spb;
367  int         cmp, eofa, eofb;
368
369  initspan(&spa);
370  initspan(&spb);
371  cmp = eofa = eofb = 0;
372
373  addtospan(&spa, fa, RESET);
374  addtospan(&spb, fb, RESET);
375
376  while (spa.nl && spb.nl) {
377    if ((cmp = jlinecmp(spa.lines[0], spb.lines[0], jfa, jfb)) < 0) {
378      if (unpairsa)
379        prline(spa.lines[0]);
380      if (!addtospan(&spa, fa, RESET)) {
381        if (unpairsb) { /* a is EOF'd; print the rest of
382               b */
383          do
384            prline(spb.lines[0]);
385          while (addtospan(&spb, fb, RESET));
386        }
387        eofa = eofb = 1;
388      } else {
389        continue;
390      }
391    } else if (cmp > 0) {
392      if (unpairsb)
393        prline(spb.lines[0]);
394      if (!addtospan(&spb, fb, RESET)) {
395        if (unpairsa) { /* b is EOF'd; print the rest of
396               a */
397          do
398            prline(spa.lines[0]);
399          while (addtospan(&spa, fa, RESET));
400        }
401        eofa = eofb = 1;
402      } else {
403        continue;
404      }
405    } else if (cmp == 0) {
406      /* read all consecutive matching lines from a */
407      do {
408        if (!addtospan(&spa, fa, EXPAND)) {
409          eofa = 1;
410          spa.nl++;
411          break;
412        }
413      } while (jlinecmp(spa.lines[spa.nl - 1], spb.lines[0], jfa, jfb) == 0);
414
415      /* read all consecutive matching lines from b */
416      do {
417        if (!addtospan(&spb, fb, EXPAND)) {
418          eofb = 1;
419          spb.nl++;
420          break;
421        }
422      } while (jlinecmp(spa.lines[0], spb.lines[spb.nl - 1], jfa, jfb) == 0);
423
424      if (pairs)
425        prspanjoin(&spa, &spb, jfa, jfb);
426
427    } else { /* FIELD_ERROR: both lines lacked join fields */
428      if (unpairsa)
429        prline(spa.lines[0]);
430      if (unpairsb)
431        prline(spb.lines[0]);
432      eofa = addtospan(&spa, fa, RESET) ? 0 : 1;
433      eofb = addtospan(&spb, fb, RESET) ? 0 : 1;
434      if (!eofa && !eofb)
435        continue;
436    }
437
438    if (eofa) {
439      spa.nl = 0;
440    } else {
441      swaplines(spa.lines[0], spa.lines[spa.nl - 1]); /* ugly */
442      spa.nl = 1;
443    }
444
445    if (eofb) {
446      spb.nl = 0;
447    } else {
448      swaplines(spb.lines[0], spb.lines[spb.nl - 1]); /* ugly */
449      spb.nl = 1;
450    }
451  }
452  freespan(&spa);
453  freespan(&spb);
454}
455
456// ?man join: join lines on common field
457// ?man join lines of two sorted files on a common field
458int
459main(int argc, char *argv[])
460{
461  size_t jf[2] = {
462      jfield,
463      jfield,
464  };
465  FILE *fp[2];
466  int   ret = 0, n;
467  char *fno;
468
469  ARGBEGIN
470  {
471    // ?man -1:num: specify option flag
472    case '1':
473      jf[0] = estrtonum(
474          EARGF(usage()), 1, MIN((unsigned long long)LLONG_MAX, (unsigned long long)SIZE_MAX)
475      );
476      break;
477    // ?man -2:num: specify option flag
478    case '2':
479      jf[1] = estrtonum(
480          EARGF(usage()), 1, MIN((unsigned long long)LLONG_MAX, (unsigned long long)SIZE_MAX)
481      );
482      break;
483    // ?man -a:str: print or show all entries
484    case 'a':
485      fno = EARGF(usage());
486      if (strcmp(fno, "1") == 0)
487        unpairsa = 1;
488      else if (strcmp(fno, "2") == 0)
489        unpairsb = 1;
490      else
491        usage();
492      break;
493    // ?man -e:str: specify expression or pattern
494    case 'e':
495      replace = EARGF(usage());
496      break;
497    // ?man -o:str: specify output file
498    case 'o':
499      oflag = 1;
500      initolist(&output);
501      makeolist(&output, EARGF(usage()));
502      break;
503    // ?man -t:str: sort or specify timestamp
504    case 't':
505      sep = EARGF(usage());
506      break;
507    // ?man -v:str: verbose mode; show progress
508    case 'v':
509      pairs = 0;
510      fno   = EARGF(usage());
511      if (strcmp(fno, "1") == 0)
512        unpairsa = 1;
513      else if (strcmp(fno, "2") == 0)
514        unpairsb = 1;
515      else
516        usage();
517      break;
518    default:
519      usage();
520  }
521  ARGEND
522
523  if (sep)
524    seplen = unescape(sep);
525
526  if (argc != 2)
527    usage();
528
529  for (n = 0; n < 2; n++) {
530    if (!strcmp(argv[n], "-")) {
531      argv[n] = "<stdin>";
532      fp[n]   = stdin;
533    } else if (!(fp[n] = fopen(argv[n], "r"))) {
534      eprintf("fopen %s:", argv[n]);
535    }
536  }
537
538  jf[0]--;
539  jf[1]--;
540
541  join(fp[0], fp[1], jf[0], jf[1]);
542
543  if (oflag)
544    freespecs(&output);
545
546  if (fshut(fp[0], argv[0]) | (fp[0] != fp[1] && fshut(fp[1], argv[1])) | fshut(stdout, "<stdout>"))
547    ret = 2;
548
549  return ret;
550}