1/*-
2 * SPDX-License-Identifier: BSD-3-Clause
3 *
4 * Copyright (c) 1991, 1993, 1994
5 *	The Regents of the University of California.  All rights reserved.
6 *
7 * This code is derived from software contributed to Berkeley by
8 * Steve Hayman of the Computer Science Department, Indiana University,
9 * Michiro Hikida and David Goodenough.
10 *
11 * Redistribution and use in source and binary forms, with or without
12 * modification, are permitted provided that the following conditions
13 * are met:
14 * 1. Redistributions of source code must retain the above copyright
15 *    notice, this list of conditions and the following disclaimer.
16 * 2. Redistributions in binary form must reproduce the above copyright
17 *    notice, this list of conditions and the following disclaimer in the
18 *    documentation and/or other materials provided with the distribution.
19 * 3. Neither the name of the University nor the names of its contributors
20 *    may be used to endorse or promote products derived from this software
21 *    without specific prior written permission.
22 *
23 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
24 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
27 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
29 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
30 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
32 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
33 * SUCH DAMAGE.
34 */
35
36#ifndef lint
37static const char copyright[] =
38"@(#) Copyright (c) 1991, 1993, 1994\n\
39	The Regents of the University of California.  All rights reserved.\n";
40#endif /* not lint */
41
42#ifndef lint
43#if 0
44static char sccsid[] = "@(#)join.c	8.6 (Berkeley) 5/4/95";
45#endif
46#endif /* not lint */
47#include <sys/cdefs.h>
48__FBSDID("$FreeBSD$");
49
50#include <sys/param.h>
51
52#include <err.h>
53#include <errno.h>
54#include <limits.h>
55#include <locale.h>
56#include <stdio.h>
57#include <stdlib.h>
58#include <string.h>
59#include <unistd.h>
60#include <wchar.h>
61
62/*
63 * There's a structure per input file which encapsulates the state of the
64 * file.  We repeatedly read lines from each file until we've read in all
65 * the consecutive lines from the file with a common join field.  Then we
66 * compare the set of lines with an equivalent set from the other file.
67 */
68typedef struct {
69	char *line;		/* line */
70	u_long linealloc;	/* line allocated count */
71	char **fields;		/* line field(s) */
72	u_long fieldcnt;	/* line field(s) count */
73	u_long fieldalloc;	/* line field(s) allocated count */
74} LINE;
75
76typedef struct {
77	FILE *fp;		/* file descriptor */
78	u_long joinf;		/* join field (-1, -2, -j) */
79	int unpair;		/* output unpairable lines (-a) */
80	u_long number;		/* 1 for file 1, 2 for file 2 */
81
82	LINE *set;		/* set of lines with same field */
83	int pushbool;		/* if pushback is set */
84	u_long pushback;	/* line on the stack */
85	u_long setcnt;		/* set count */
86	u_long setalloc;	/* set allocated count */
87} INPUT;
88static INPUT input1 = { NULL, 0, 0, 1, NULL, 0, 0, 0, 0 },
89    input2 = { NULL, 0, 0, 2, NULL, 0, 0, 0, 0 };
90
91typedef struct {
92	u_long	filenum;	/* file number */
93	u_long	fieldno;	/* field number */
94} OLIST;
95static OLIST *olist;		/* output field list */
96static u_long olistcnt;		/* output field list count */
97static u_long olistalloc;	/* output field allocated count */
98
99static int joinout = 1;		/* show lines with matched join fields (-v) */
100static int needsep;		/* need separator character */
101static int spans = 1;		/* span multiple delimiters (-t) */
102static char *empty;		/* empty field replacement string (-e) */
103static wchar_t default_tabchar[] = L" \t";
104static wchar_t *tabchar = default_tabchar; /* delimiter characters (-t) */
105
106static int  cmp(LINE *, u_long, LINE *, u_long);
107static void fieldarg(char *);
108static void joinlines(INPUT *, INPUT *);
109static int  mbscoll(const char *, const char *);
110static char *mbssep(char **, const wchar_t *);
111static void obsolete(char **);
112static void outfield(LINE *, u_long, int);
113static void outoneline(INPUT *, LINE *);
114static void outtwoline(INPUT *, LINE *, INPUT *, LINE *);
115static void slurp(INPUT *);
116static wchar_t *towcs(const char *);
117static void usage(void);
118
119int
120main(int argc, char *argv[])
121{
122	INPUT *F1, *F2;
123	int aflag, ch, cval, vflag;
124	char *end;
125
126	setlocale(LC_ALL, "");
127
128	F1 = &input1;
129	F2 = &input2;
130
131	aflag = vflag = 0;
132	obsolete(argv);
133	while ((ch = getopt(argc, argv, "\01a:e:j:1:2:o:t:v:")) != -1) {
134		switch (ch) {
135		case '\01':		/* See comment in obsolete(). */
136			aflag = 1;
137			F1->unpair = F2->unpair = 1;
138			break;
139		case '1':
140			if ((F1->joinf = strtol(optarg, &end, 10)) < 1)
141				errx(1, "-1 option field number less than 1");
142			if (*end)
143				errx(1, "illegal field number -- %s", optarg);
144			--F1->joinf;
145			break;
146		case '2':
147			if ((F2->joinf = strtol(optarg, &end, 10)) < 1)
148				errx(1, "-2 option field number less than 1");
149			if (*end)
150				errx(1, "illegal field number -- %s", optarg);
151			--F2->joinf;
152			break;
153		case 'a':
154			aflag = 1;
155			switch(strtol(optarg, &end, 10)) {
156			case 1:
157				F1->unpair = 1;
158				break;
159			case 2:
160				F2->unpair = 1;
161				break;
162			default:
163				errx(1, "-a option file number not 1 or 2");
164				break;
165			}
166			if (*end)
167				errx(1, "illegal file number -- %s", optarg);
168			break;
169		case 'e':
170			empty = optarg;
171			break;
172		case 'j':
173			if ((F1->joinf = F2->joinf =
174			    strtol(optarg, &end, 10)) < 1)
175				errx(1, "-j option field number less than 1");
176			if (*end)
177				errx(1, "illegal field number -- %s", optarg);
178			--F1->joinf;
179			--F2->joinf;
180			break;
181		case 'o':
182			fieldarg(optarg);
183			break;
184		case 't':
185			spans = 0;
186			if (mbrtowc(&tabchar[0], optarg, MB_LEN_MAX, NULL) !=
187			    strlen(optarg))
188				errx(1, "illegal tab character specification");
189			tabchar[1] = L'\0';
190			break;
191		case 'v':
192			vflag = 1;
193			joinout = 0;
194			switch (strtol(optarg, &end, 10)) {
195			case 1:
196				F1->unpair = 1;
197				break;
198			case 2:
199				F2->unpair = 1;
200				break;
201			default:
202				errx(1, "-v option file number not 1 or 2");
203				break;
204			}
205			if (*end)
206				errx(1, "illegal file number -- %s", optarg);
207			break;
208		case '?':
209		default:
210			usage();
211		}
212	}
213	argc -= optind;
214	argv += optind;
215
216	if (aflag && vflag)
217		errx(1, "the -a and -v options are mutually exclusive");
218
219	if (argc != 2)
220		usage();
221
222	/* Open the files; "-" means stdin. */
223	if (!strcmp(*argv, "-"))
224		F1->fp = stdin;
225	else if ((F1->fp = fopen(*argv, "r")) == NULL)
226		err(1, "%s", *argv);
227	++argv;
228	if (!strcmp(*argv, "-"))
229		F2->fp = stdin;
230	else if ((F2->fp = fopen(*argv, "r")) == NULL)
231		err(1, "%s", *argv);
232	if (F1->fp == stdin && F2->fp == stdin)
233		errx(1, "only one input file may be stdin");
234
235	slurp(F1);
236	slurp(F2);
237	while (F1->setcnt && F2->setcnt) {
238		cval = cmp(F1->set, F1->joinf, F2->set, F2->joinf);
239		if (cval == 0) {
240			/* Oh joy, oh rapture, oh beauty divine! */
241			if (joinout)
242				joinlines(F1, F2);
243			slurp(F1);
244			slurp(F2);
245		} else if (cval < 0) {
246			/* File 1 takes the lead... */
247			if (F1->unpair)
248				joinlines(F1, NULL);
249			slurp(F1);
250		} else {
251			/* File 2 takes the lead... */
252			if (F2->unpair)
253				joinlines(F2, NULL);
254			slurp(F2);
255		}
256	}
257
258	/*
259	 * Now that one of the files is used up, optionally output any
260	 * remaining lines from the other file.
261	 */
262	if (F1->unpair)
263		while (F1->setcnt) {
264			joinlines(F1, NULL);
265			slurp(F1);
266		}
267	if (F2->unpair)
268		while (F2->setcnt) {
269			joinlines(F2, NULL);
270			slurp(F2);
271		}
272	exit(0);
273}
274
275static void
276slurp(INPUT *F)
277{
278	LINE *lp, *lastlp, tmp;
279	size_t len;
280	int cnt;
281	char *bp, *fieldp;
282
283	/*
284	 * Read all of the lines from an input file that have the same
285	 * join field.
286	 */
287	F->setcnt = 0;
288	for (lastlp = NULL;; ++F->setcnt) {
289		/*
290		 * If we're out of space to hold line structures, allocate
291		 * more.  Initialize the structure so that we know that this
292		 * is new space.
293		 */
294		if (F->setcnt == F->setalloc) {
295			cnt = F->setalloc;
296			F->setalloc += 50;
297			if ((F->set = realloc(F->set,
298			    F->setalloc * sizeof(LINE))) == NULL)
299				err(1, NULL);
300			memset(F->set + cnt, 0, 50 * sizeof(LINE));
301
302			/* re-set lastlp in case it moved */
303			if (lastlp != NULL)
304				lastlp = &F->set[F->setcnt - 1];
305		}
306
307		/*
308		 * Get any pushed back line, else get the next line.  Allocate
309		 * space as necessary.  If taking the line from the stack swap
310		 * the two structures so that we don't lose space allocated to
311		 * either structure.  This could be avoided by doing another
312		 * level of indirection, but it's probably okay as is.
313		 */
314		lp = &F->set[F->setcnt];
315		if (F->setcnt)
316			lastlp = &F->set[F->setcnt - 1];
317		if (F->pushbool) {
318			tmp = F->set[F->setcnt];
319			F->set[F->setcnt] = F->set[F->pushback];
320			F->set[F->pushback] = tmp;
321			F->pushbool = 0;
322			continue;
323		}
324		if ((bp = fgetln(F->fp, &len)) == NULL)
325			return;
326		if (lp->linealloc <= len + 1) {
327			lp->linealloc += MAX(100, len + 1 - lp->linealloc);
328			if ((lp->line =
329			    realloc(lp->line, lp->linealloc)) == NULL)
330				err(1, NULL);
331		}
332		memmove(lp->line, bp, len);
333
334		/* Replace trailing newline, if it exists. */
335		if (bp[len - 1] == '\n')
336			lp->line[len - 1] = '\0';
337		else
338			lp->line[len] = '\0';
339		bp = lp->line;
340
341		/* Split the line into fields, allocate space as necessary. */
342		lp->fieldcnt = 0;
343		while ((fieldp = mbssep(&bp, tabchar)) != NULL) {
344			if (spans && *fieldp == '\0')
345				continue;
346			if (lp->fieldcnt == lp->fieldalloc) {
347				lp->fieldalloc += 50;
348				if ((lp->fields = realloc(lp->fields,
349				    lp->fieldalloc * sizeof(char *))) == NULL)
350					err(1, NULL);
351			}
352			lp->fields[lp->fieldcnt++] = fieldp;
353		}
354
355		/* See if the join field value has changed. */
356		if (lastlp != NULL && cmp(lp, F->joinf, lastlp, F->joinf)) {
357			F->pushbool = 1;
358			F->pushback = F->setcnt;
359			break;
360		}
361	}
362}
363
364static char *
365mbssep(char **stringp, const wchar_t *delim)
366{
367	char *s, *tok;
368	const wchar_t *spanp;
369	wchar_t c, sc;
370	size_t n;
371
372	if ((s = *stringp) == NULL)
373		return (NULL);
374	for (tok = s;;) {
375		n = mbrtowc(&c, s, MB_LEN_MAX, NULL);
376		if (n == (size_t)-1 || n == (size_t)-2)
377			errc(1, EILSEQ, NULL);	/* XXX */
378		s += n;
379		spanp = delim;
380		do {
381			if ((sc = *spanp++) == c) {
382				if (c == 0)
383					s = NULL;
384				else
385					s[-n] = '\0';
386				*stringp = s;
387				return (tok);
388			}
389		} while (sc != 0);
390	}
391}
392
393static int
394cmp(LINE *lp1, u_long fieldno1, LINE *lp2, u_long fieldno2)
395{
396	if (lp1->fieldcnt <= fieldno1)
397		return (lp2->fieldcnt <= fieldno2 ? 0 : 1);
398	if (lp2->fieldcnt <= fieldno2)
399		return (-1);
400	return (mbscoll(lp1->fields[fieldno1], lp2->fields[fieldno2]));
401}
402
403static int
404mbscoll(const char *s1, const char *s2)
405{
406	wchar_t *w1, *w2;
407	int ret;
408
409	if (MB_CUR_MAX == 1)
410		return (strcoll(s1, s2));
411	if ((w1 = towcs(s1)) == NULL || (w2 = towcs(s2)) == NULL)
412		err(1, NULL);	/* XXX */
413	ret = wcscoll(w1, w2);
414	free(w1);
415	free(w2);
416	return (ret);
417}
418
419static wchar_t *
420towcs(const char *s)
421{
422	wchar_t *wcs;
423	size_t n;
424
425	if ((n = mbsrtowcs(NULL, &s, 0, NULL)) == (size_t)-1)
426		return (NULL);
427	if ((wcs = malloc((n + 1) * sizeof(*wcs))) == NULL)
428		return (NULL);
429	mbsrtowcs(wcs, &s, n + 1, NULL);
430	return (wcs);
431}
432
433static void
434joinlines(INPUT *F1, INPUT *F2)
435{
436	u_long cnt1, cnt2;
437
438	/*
439	 * Output the results of a join comparison.  The output may be from
440	 * either file 1 or file 2 (in which case the first argument is the
441	 * file from which to output) or from both.
442	 */
443	if (F2 == NULL) {
444		for (cnt1 = 0; cnt1 < F1->setcnt; ++cnt1)
445			outoneline(F1, &F1->set[cnt1]);
446		return;
447	}
448	for (cnt1 = 0; cnt1 < F1->setcnt; ++cnt1)
449		for (cnt2 = 0; cnt2 < F2->setcnt; ++cnt2)
450			outtwoline(F1, &F1->set[cnt1], F2, &F2->set[cnt2]);
451}
452
453static void
454outoneline(INPUT *F, LINE *lp)
455{
456	u_long cnt;
457
458	/*
459	 * Output a single line from one of the files, according to the
460	 * join rules.  This happens when we are writing unmatched single
461	 * lines.  Output empty fields in the right places.
462	 */
463	if (olist)
464		for (cnt = 0; cnt < olistcnt; ++cnt) {
465			if (olist[cnt].filenum == (unsigned)F->number)
466				outfield(lp, olist[cnt].fieldno, 0);
467			else if (olist[cnt].filenum == 0)
468				outfield(lp, F->joinf, 0);
469			else
470				outfield(lp, 0, 1);
471		}
472	else {
473		/*
474		 * Output the join field, then the remaining fields.
475		 */
476		outfield(lp, F->joinf, 0);
477		for (cnt = 0; cnt < lp->fieldcnt; ++cnt)
478			if (F->joinf != cnt)
479				outfield(lp, cnt, 0);
480	}
481	(void)printf("\n");
482	if (ferror(stdout))
483		err(1, "stdout");
484	needsep = 0;
485}
486
487static void
488outtwoline(INPUT *F1, LINE *lp1, INPUT *F2, LINE *lp2)
489{
490	u_long cnt;
491
492	/* Output a pair of lines according to the join list (if any). */
493	if (olist)
494		for (cnt = 0; cnt < olistcnt; ++cnt)
495			if (olist[cnt].filenum == 0) {
496				if (lp1->fieldcnt >= F1->joinf)
497					outfield(lp1, F1->joinf, 0);
498				else
499					outfield(lp2, F2->joinf, 0);
500			} else if (olist[cnt].filenum == 1)
501				outfield(lp1, olist[cnt].fieldno, 0);
502			else /* if (olist[cnt].filenum == 2) */
503				outfield(lp2, olist[cnt].fieldno, 0);
504	else {
505		/*
506		 * Output the join field, then the remaining fields from F1
507		 * and F2.
508		 */
509		outfield(lp1, F1->joinf, 0);
510		for (cnt = 0; cnt < lp1->fieldcnt; ++cnt)
511			if (F1->joinf != cnt)
512				outfield(lp1, cnt, 0);
513		for (cnt = 0; cnt < lp2->fieldcnt; ++cnt)
514			if (F2->joinf != cnt)
515				outfield(lp2, cnt, 0);
516	}
517	(void)printf("\n");
518	if (ferror(stdout))
519		err(1, "stdout");
520	needsep = 0;
521}
522
523static void
524outfield(LINE *lp, u_long fieldno, int out_empty)
525{
526	if (needsep++)
527		(void)printf("%lc", (wint_t)*tabchar);
528	if (!ferror(stdout)) {
529		if (lp->fieldcnt <= fieldno || out_empty) {
530			if (empty != NULL)
531				(void)printf("%s", empty);
532		} else {
533			if (*lp->fields[fieldno] == '\0')
534				return;
535			(void)printf("%s", lp->fields[fieldno]);
536		}
537	}
538	if (ferror(stdout))
539		err(1, "stdout");
540}
541
542/*
543 * Convert an output list argument "2.1, 1.3, 2.4" into an array of output
544 * fields.
545 */
546static void
547fieldarg(char *option)
548{
549	u_long fieldno, filenum;
550	char *end, *token;
551
552	while ((token = strsep(&option, ", \t")) != NULL) {
553		if (*token == '\0')
554			continue;
555		if (token[0] == '0')
556			filenum = fieldno = 0;
557		else if ((token[0] == '1' || token[0] == '2') &&
558		    token[1] == '.') {
559			filenum = token[0] - '0';
560			fieldno = strtol(token + 2, &end, 10);
561			if (*end)
562				errx(1, "malformed -o option field");
563			if (fieldno == 0)
564				errx(1, "field numbers are 1 based");
565			--fieldno;
566		} else
567			errx(1, "malformed -o option field");
568		if (olistcnt == olistalloc) {
569			olistalloc += 50;
570			if ((olist = realloc(olist,
571			    olistalloc * sizeof(OLIST))) == NULL)
572				err(1, NULL);
573		}
574		olist[olistcnt].filenum = filenum;
575		olist[olistcnt].fieldno = fieldno;
576		++olistcnt;
577	}
578}
579
580static void
581obsolete(char **argv)
582{
583	size_t len;
584	char **p, *ap, *t;
585
586	while ((ap = *++argv) != NULL) {
587		/* Return if "--". */
588		if (ap[0] == '-' && ap[1] == '-')
589			return;
590		/* skip if not an option */
591		if (ap[0] != '-')
592			continue;
593		switch (ap[1]) {
594		case 'a':
595			/*
596			 * The original join allowed "-a", which meant the
597			 * same as -a1 plus -a2.  POSIX 1003.2, Draft 11.2
598			 * only specifies this as "-a 1" and "a -2", so we
599			 * have to use another option flag, one that is
600			 * unlikely to ever be used or accidentally entered
601			 * on the command line.  (Well, we could reallocate
602			 * the argv array, but that hardly seems worthwhile.)
603			 */
604			if (ap[2] == '\0' && (argv[1] == NULL ||
605			    (strcmp(argv[1], "1") != 0 &&
606			    strcmp(argv[1], "2") != 0))) {
607				ap[1] = '\01';
608				warnx("-a option used without an argument; "
609				    "reverting to historical behavior");
610			}
611			break;
612		case 'j':
613			/*
614			 * The original join allowed "-j[12] arg" and "-j arg".
615			 * Convert the former to "-[12] arg".  Don't convert
616			 * the latter since getopt(3) can handle it.
617			 */
618			switch(ap[2]) {
619			case '1':
620				if (ap[3] != '\0')
621					goto jbad;
622				ap[1] = '1';
623				ap[2] = '\0';
624				break;
625			case '2':
626				if (ap[3] != '\0')
627					goto jbad;
628				ap[1] = '2';
629				ap[2] = '\0';
630				break;
631			case '\0':
632				break;
633			default:
634jbad:				errx(1, "illegal option -- %s", ap);
635				usage();
636			}
637			break;
638		case 'o':
639			/*
640			 * The original join allowed "-o arg arg".
641			 * Convert to "-o arg -o arg".
642			 */
643			if (ap[2] != '\0')
644				break;
645			for (p = argv + 2; *p; ++p) {
646				if (p[0][0] == '0' || ((p[0][0] != '1' &&
647				    p[0][0] != '2') || p[0][1] != '.'))
648					break;
649				len = strlen(*p);
650				if (len - 2 != strspn(*p + 2, "0123456789"))
651					break;
652				if ((t = malloc(len + 3)) == NULL)
653					err(1, NULL);
654				t[0] = '-';
655				t[1] = 'o';
656				memmove(t + 2, *p, len + 1);
657				*p = t;
658			}
659			argv = p - 1;
660			break;
661		}
662	}
663}
664
665static void
666usage(void)
667{
668	(void)fprintf(stderr, "%s %s\n%s\n",
669	    "usage: join [-a fileno | -v fileno ] [-e string] [-1 field]",
670	    "[-2 field]",
671		"            [-o list] [-t char] file1 file2");
672	exit(1);
673}
674