1 /***********************************************************************
2 *                                                                      *
3 *               This software is part of the ast package               *
4 *          Copyright (c) 1992-2008 AT&T Intellectual Property          *
5 *                      and is licensed under the                       *
6 *                  Common Public License, Version 1.0                  *
7 *                    by AT&T Intellectual Property                     *
8 *                                                                      *
9 *                A copy of the License is available at                 *
10 *            http://www.opensource.org/licenses/cpl1.0.txt             *
11 *         (with md5 checksum 059e8cd6165cb4c31e351f2b69388fd9)         *
12 *                                                                      *
13 *              Information and Software Systems Research               *
14 *                            AT&T Research                             *
15 *                           Florham Park NJ                            *
16 *                                                                      *
17 *                 Glenn Fowler <gsf@research.att.com>                  *
18 *                  David Korn <dgk@research.att.com>                   *
19 *                                                                      *
20 ***********************************************************************/
21 #pragma prototyped
22 /*
23  * David Korn
24  * Glenn Fowler
25  * AT&T Research
26  *
27  * join
28  */
29 
30 static const char usage[] =
31 "[-?\n@(#)$Id: join (AT&T Research) 2006-10-31 $\n]"
32 USAGE_LICENSE
33 "[+NAME?join - relational database operator]"
34 "[+DESCRIPTION?\bjoin\b performs an \aequality join\a on the files \afile1\a "
35 	"and \afile2\a and writes the resulting joined files to standard "
36 	"output.  By default, a field is delimited by one or more spaces "
37 	"and tabs with leading spaces and/or tabs ignored.  The \b-t\b option "
38 	"can be used to change the field delimiter.]"
39 "[+?The \ajoin field\a is a field in each file on which files are compared. "
40 	"By default \bjoin\b writes one line in the output for each pair "
41 	"of lines in \afiles1\a and \afiles2\a that have identical join "
42 	"fields.  The default output line consists of the join field, "
43 	"then the remaining fields from \afile1\a, then the remaining "
44 	"fields from \afile2\a, but this can be changed with the \b-o\b "
45 	"option.  The \b-a\b option can be used to add unmatched lines "
46 	"to the output.  The \b-v\b option can be used to output only "
47 	"unmatched lines.]"
48 "[+?The files \afile1\a and \afile2\a must be ordered in the collating "
49 	"sequence of \bsort -b\b on the fields on which they are to be "
50 	"joined otherwise the results are unspecified.]"
51 "[+?If either \afile1\a or \afile2\a is \b-\b, \bjoin\b "
52         "uses standard input starting at the current location.]"
53 
54 "[e:empty]:[string?Replace empty output fields in the list selected with"
55 "	\b-o\b with \astring\a.]"
56 "[o:output]:[list?Construct the output line to comprise the fields specified "
57 	"in a blank or comma separated list \alist\a.  Each element in "
58 	"\alist\a consists of a file number (either 1 or 2), a period, "
59 	"and a field number or \b0\b representing the join field.  "
60 	"As an obsolete feature multiple occurrences of \b-o\b can "
61 	"be specified.]"
62 "[t:separator|tabs]:[delim?Use \adelim\a as the field separator for both input"
63 "	and output.]"
64 "[1:j1]#[field?Join on field \afield\a of \afile1\a.  Fields start at 1.]"
65 "[2:j2]#[field?Join on field \afield\a of \afile2\a.  Fields start at 1.]"
66 "[j:join]#[field?Equivalent to \b-1\b \afield\a \b-2\b \afield\a.]"
67 "[a:unpairable]#[fileno?Write a line for each unpairable line in file"
68 "	\afileno\a, where \afileno\a is either 1 or 2, in addition to the"
69 "	normal output.  If \b-a\b options appear for both 1 and 2, then "
70 	"all unpairable lines will be output.]"
71 "[v:suppress]#[fileno?Write a line for each unpairable line in file"
72 "	\afileno\a, where \afileno\a is either 1 or 2, instead of the normal "
73 	"output.  If \b-v\b options appear for both 1 and 2, then "
74 	"all unpairable lines will be output.] ]"
75 "[i:ignorecase?Ignore case in field comparisons.]"
76 "[B!:mmap?Enable memory mapped reads instead of buffered.]"
77 
78 "[+?The following obsolete option forms are also recognized: \b-j\b \afield\a"
79 "	is equivalent to \b-1\b \afield\a \b-2\b \afield\a, \b-j1\b \afield\a"
80 "	is equivalent to \b-1\b \afield\a, and \b-j2\b \afield\a is"
81 "	equivalent to \b-2\b \afield\a.]"
82 
83 "\n"
84 "\nfile1 file2\n"
85 "\n"
86 "[+EXIT STATUS?]{"
87 	"[+0?Both files processed successfully.]"
88 	"[+>0?An error occurred.]"
89 "}"
90 "[+SEE ALSO?\bcut\b(1), \bcomm\b(1), \bpaste\b(1), \bsort\b(1), \buniq\b(1)]"
91 ;
92 
93 #include <cmd.h>
94 #include <sfdisc.h>
95 
96 #define C_FILE1		001
97 #define C_FILE2		002
98 #define C_COMMON	004
99 #define C_ALL		(C_FILE1|C_FILE2|C_COMMON)
100 
101 #define NFIELD		10
102 #define JOINFIELD	2
103 
104 #define S_DELIM		1
105 #define S_SPACE		2
106 #define S_NL		3
107 
108 typedef struct
109 {
110 	Sfio_t*		iop;
111 	char*		name;
112 	char*		recptr;
113 	int		reclen;
114 	int		field;
115 	int		fieldlen;
116 	int		nfields;
117 	int		maxfields;
118 	int		spaces;
119 	int		hit;
120 	int		discard;
121 	char**		fieldlist;
122 } File_t;
123 
124 typedef struct
125 {
126 	unsigned char	state[1<<CHAR_BIT];
127 	Sfio_t*		outfile;
128 	int*		outlist;
129 	int		outmode;
130 	int		ooutmode;
131 	char*		nullfield;
132 	int		delim;
133 	int		buffered;
134 	int		ignorecase;
135 	char*		same;
136 	int		samesize;
137 	void*		context;
138 	File_t		file[2];
139 } Join_t;
140 
141 static void
142 done(register Join_t* jp)
143 {
144 	if (jp->file[0].iop && jp->file[0].iop != sfstdin)
145 		sfclose(jp->file[0].iop);
146 	if (jp->file[1].iop && jp->file[1].iop != sfstdin)
147 		sfclose(jp->file[1].iop);
148 	if (jp->outlist)
149 		free(jp->outlist);
150 	if (jp->file[0].fieldlist)
151 		free(jp->file[0].fieldlist);
152 	if (jp->file[1].fieldlist)
153 		free(jp->file[1].fieldlist);
154 	if (jp->same)
155 		free(jp->same);
156 	free(jp);
157 }
158 
159 static Join_t*
160 init(void)
161 {
162 	register Join_t*	jp;
163 
164 	if (jp = newof(0, Join_t, 1, 0))
165 	{
166 		jp->state[' '] = jp->state['\t'] = S_SPACE;
167 		jp->delim = -1;
168 		jp->nullfield = 0;
169 		if (!(jp->file[0].fieldlist = newof(0, char*, NFIELD + 1, 0)) ||
170 		    !(jp->file[1].fieldlist = newof(0, char*, NFIELD + 1, 0)))
171 		{
172 			done(jp);
173 			return 0;
174 		}
175 		jp->file[0].maxfields = NFIELD;
176 		jp->file[1].maxfields = NFIELD;
177 		jp->outmode = C_COMMON;
178 	}
179 	return jp;
180 }
181 
182 static int
183 getolist(Join_t* jp, const char* first, char** arglist)
184 {
185 	register const char*	cp = first;
186 	char**			argv = arglist;
187 	register int		c;
188 	int*			outptr;
189 	int*			outmax;
190 	int			nfield = NFIELD;
191 	char*			str;
192 
193 	outptr = jp->outlist = newof(0, int, NFIELD + 1, 0);
194 	outmax = outptr + NFIELD;
195 	while (c = *cp++)
196 	{
197 		if (c==' ' || c=='\t' || c==',')
198 			continue;
199 		str = (char*)--cp;
200 		if (*cp=='0' && ((c=cp[1])==0 || c==' ' || c=='\t' || c==','))
201 		{
202 			str++;
203 			c = JOINFIELD;
204 			goto skip;
205 		}
206 		if (cp[1]!='.' || (*cp!='1' && *cp!='2') || (c=strtol(cp+2,&str,10)) <=0)
207 		{
208 			error(2,"%s: invalid field list",first);
209 			break;
210 		}
211 		c--;
212 		c <<=2;
213 		if (*cp=='2')
214 			c |=1;
215 	skip:
216 		if (outptr >= outmax)
217 		{
218 			jp->outlist = newof(jp->outlist, int, 2 * nfield + 1, 0);
219 			outptr = jp->outlist + nfield;
220 			nfield *= 2;
221 			outmax = jp->outlist + nfield;
222 		}
223 		*outptr++ = c;
224 		cp = str;
225 	}
226 	/* need to accept obsolescent command syntax */
227 	while (1)
228 	{
229 		if (!(cp= *argv) || cp[1]!='.' || (*cp!='1' && *cp!='2'))
230 		{
231 			if (*cp=='0' && cp[1]==0)
232 			{
233 				c = JOINFIELD;
234 				goto skip2;
235 			}
236 			break;
237 		}
238 		str = (char*)cp;
239 		c = strtol(cp+2, &str,10);
240 		if (*str || --c<0)
241 			break;
242 		argv++;
243 		c <<= 2;
244 		if (*cp=='2')
245 			c |=1;
246 	skip2:
247 		if (outptr >= outmax)
248 		{
249 			jp->outlist = newof(jp->outlist, int, 2 * nfield + 1, 0);
250 			outptr = jp->outlist + nfield;
251 			nfield *= 2;
252 			outmax = jp->outlist + nfield;
253 		}
254 		*outptr++ = c;
255 	}
256 	*outptr = -1;
257 	return argv-arglist;
258 }
259 
260 /*
261  * read in a record from file <index> and split into fields
262  */
263 static unsigned char*
264 getrec(Join_t* jp, int index, int discard)
265 {
266 	register unsigned char*	sp = jp->state;
267 	register File_t*	fp = &jp->file[index];
268 	register char**		ptr = fp->fieldlist;
269 	register char**		ptrmax = ptr + fp->maxfields;
270 	register char*		cp;
271 	register int		n = 0;
272 
273 	if (sh_checksig(jp->context))
274 		return 0;
275 	if (discard && fp->discard)
276 		sfraise(fp->iop, SFSK_DISCARD, NiL);
277 	fp->spaces = 0;
278 	fp->hit = 0;
279 	if (!(cp = sfgetr(fp->iop, '\n', 0)))
280 	{
281 		jp->outmode &= ~(1<<index);
282 		return 0;
283 	}
284 	fp->recptr = cp;
285 	fp->reclen = sfvalue(fp->iop);
286 	if (jp->delim=='\n')	/* handle new-line delimiter specially */
287 	{
288 		*ptr++ = cp;
289 		cp += fp->reclen;
290 	}
291 	else while (n!=S_NL) /* separate into fields */
292 	{
293 		if (ptr >= ptrmax)
294 		{
295 			n = 2*fp->maxfields;
296 			fp->fieldlist = newof(fp->fieldlist, char*, n + 1, 0);
297 			ptr = fp->fieldlist + fp->maxfields;
298 			fp->maxfields = n;
299 			ptrmax = fp->fieldlist+n;
300 		}
301 		*ptr++ = cp;
302 		if (jp->delim<=0 && sp[*(unsigned char*)cp]==S_SPACE)
303 		{
304 			fp->spaces = 1;
305 			while (sp[*(unsigned char*)cp++]==S_SPACE);
306 			cp--;
307 		}
308 		while ((n=sp[*(unsigned char*)cp++])==0);
309 	}
310 	*ptr = cp;
311 	fp->nfields = ptr - fp->fieldlist;
312 	if ((n=fp->field) < fp->nfields)
313 	{
314 		cp = fp->fieldlist[n];
315 		/* eliminate leading spaces */
316 		if (fp->spaces)
317 		{
318 			while (sp[*(unsigned char*)cp++]==S_SPACE);
319 			cp--;
320 		}
321 		fp->fieldlen = (fp->fieldlist[n+1]-cp)-1;
322 		return (unsigned char*)cp;
323 	}
324 	fp->fieldlen = 0;
325 	return (unsigned char*)"";
326 }
327 
328 #if DEBUG_TRACE
329 static unsigned char* u1,u2,u3;
330 #define getrec(p,n,d)	(u1 = getrec(p, n, d), sfprintf(sfstdout, "[G%d#%d@%I*d:%-.8s]", __LINE__, n, sizeof(Sfoff_t), sftell(p->file[n].iop), u1), u1)
331 #endif
332 
333 /*
334  * print field <n> from file <index>
335  */
336 static int
337 outfield(Join_t* jp, int index, register int n, int last)
338 {
339 	register File_t*	fp = &jp->file[index];
340 	register char*		cp;
341 	register char*		cpmax;
342 	register int		size;
343 	register Sfio_t*	iop = jp->outfile;
344 
345 	if (n < fp->nfields)
346 	{
347 		cp = fp->fieldlist[n];
348 		cpmax = fp->fieldlist[n+1];
349 	}
350 	else
351 		cp = 0;
352 	if ((n=jp->delim)<=0)
353 	{
354 		if (fp->spaces)
355 		{
356 			/*eliminate leading spaces */
357 			while (jp->state[*(unsigned char*)cp++]==S_SPACE);
358 			cp--;
359 		}
360 		n = ' ';
361 	}
362 	if (last)
363 		n = '\n';
364 	if (cp)
365 		size = cpmax-cp;
366 	else
367 		size = 0;
368 	if (size==0)
369 	{
370 		if (!jp->nullfield)
371 			sfputc(iop,n);
372 		else if (sfputr(iop,jp->nullfield,n) < 0)
373 			return -1;
374 	}
375 	else
376 	{
377 		last = cp[size-1];
378 		cp[size-1] = n;
379 		if (sfwrite(iop,cp,size) < 0)
380 			return -1;
381 		cp[size-1] = last;
382 	}
383 	return 0;
384 }
385 
386 #if DEBUG_TRACE
387 static int i1,i2,i3;
388 #define outfield(p,i,n,f)	(sfprintf(sfstdout, "[F%d#%d:%d,%d]", __LINE__, i1=i, i2=n, i3=f), outfield(p, i1, i2, i3))
389 #endif
390 
391 static int
392 outrec(register Join_t* jp, int mode)
393 {
394 	register File_t*	fp;
395 	register int		i;
396 	register int		j;
397 	register int		k;
398 	register int		n;
399 	int*			out;
400 
401 	if (mode < 0 && jp->file[0].hit++)
402 		return 0;
403 	if (mode > 0 && jp->file[1].hit++)
404 		return 0;
405 	if (out = jp->outlist)
406 	{
407 		while ((n = *out++) >= 0)
408 		{
409 			if (n == JOINFIELD)
410 			{
411 				i = mode >= 0;
412 				j = jp->file[i].field;
413 			}
414 			else
415 			{
416 				i = n & 1;
417 				j = (mode<0 && i || mode>0 && !i) ?
418 					jp->file[i].nfields :
419 					n >> 2;
420 			}
421 			if (outfield(jp, i, j, *out < 0) < 0)
422 				return -1;
423 		}
424 		return 0;
425 	}
426 	k = jp->file[0].nfields;
427 	if (mode >= 0)
428 		k += jp->file[1].nfields - 1;
429 	for (i=0; i<2; i++)
430 	{
431 		fp = &jp->file[i];
432 		if (mode>0 && i==0)
433 		{
434 			k -= (fp->nfields - 1);
435 			continue;
436 		}
437 		n = fp->field;
438 		if (mode||i==0)
439 		{
440 			/* output join field first */
441 			if (outfield(jp,i,n,!--k) < 0)
442 				return -1;
443 			if (!k)
444 				return 0;
445 			for (j=0; j<n; j++)
446 			{
447 				if (outfield(jp,i,j,!--k) < 0)
448 					return -1;
449 				if (!k)
450 					return 0;
451 			}
452 			j = n + 1;
453 		}
454 		else
455 			j = 0;
456 		for (;j<fp->nfields; j++)
457 		{
458 			if (j!=n && outfield(jp,i,j,!--k) < 0)
459 				return -1;
460 			if (!k)
461 				return 0;
462 		}
463 	}
464 	return 0;
465 }
466 
467 #if DEBUG_TRACE
468 #define outrec(p,n)	(sfprintf(sfstdout, "[R#%d,%d,%lld,%lld:%-.*s{%d}:%-.*s{%d}]", __LINE__, i1=n, lo, hi, jp->file[0].fieldlen, cp1, jp->file[0].hit, jp->file[1].fieldlen, cp2, jp->file[1].hit), outrec(p, i1))
469 #endif
470 
471 static int
472 join(Join_t* jp)
473 {
474 	register unsigned char*	cp1;
475 	register unsigned char*	cp2;
476 	register int		n1;
477 	register int		n2;
478 	register int		n;
479 	register int		cmp;
480 	register int		same;
481 	int			o2;
482 	Sfoff_t			lo = -1;
483 	Sfoff_t			hi = -1;
484 
485 	if ((cp1 = getrec(jp, 0, 0)) && (cp2 = getrec(jp, 1, 0)) || (cp2 = 0))
486 	{
487 		n1 = jp->file[0].fieldlen;
488 		n2 = jp->file[1].fieldlen;
489 		same = 0;
490 		for (;;)
491 		{
492 			n = n1 < n2 ? n1 : n2;
493 #if DEBUG_TRACE
494 			if (!n && !(cmp = n1 < n2 ? -1 : (n1 > n2)) || n && !(cmp = (int)*cp1 - (int)*cp2) && !(cmp = jp->ignorecase ? strncasecmp((char*)cp1, (char*)cp2, n) : memcmp(cp1, cp2, n)))
495 				cmp = n1 - n2;
496 sfprintf(sfstdout, "[C#%d:%d(%c-%c),%d,%lld,%lld%s]", __LINE__, cmp, *cp1, *cp2, same, lo, hi, (jp->outmode & C_COMMON) ? ",COMMON" : "");
497 			if (!cmp)
498 #else
499 			if (!n && !(cmp = n1 < n2 ? -1 : (n1 > n2)) || n && !(cmp = (int)*cp1 - (int)*cp2) && !(cmp = jp->ignorecase ? strncasecmp((char*)cp1, (char*)cp2, n) : memcmp(cp1, cp2, n)) && !(cmp = n1 - n2))
500 #endif
501 			{
502 				if (!(jp->outmode & C_COMMON))
503 				{
504 					if (cp1 = getrec(jp, 0, 1))
505 					{
506 						n1 = jp->file[0].fieldlen;
507 						same = 1;
508 						continue;
509 					}
510 					if ((jp->ooutmode & (C_FILE1|C_FILE2)) != C_FILE2)
511 						break;
512 					if (sfseek(jp->file[0].iop, (Sfoff_t)-jp->file[0].reclen, SEEK_CUR) < 0 || !(cp1 = getrec(jp, 0, 0)))
513 					{
514 						error(ERROR_SYSTEM|2, "%s: seek error", jp->file[0].name);
515 						return -1;
516 					}
517 				}
518 				else if (outrec(jp, 0) < 0)
519 					return -1;
520 				else if (lo < 0 && (jp->outmode & C_COMMON))
521 				{
522 					if ((lo = sfseek(jp->file[1].iop, (Sfoff_t)0, SEEK_CUR)) < 0)
523 					{
524 						error(ERROR_SYSTEM|2, "%s: seek error", jp->file[1].name);
525 						return -1;
526 					}
527 					lo -= jp->file[1].reclen;
528 				}
529 				if (cp2 = getrec(jp, 1, lo < 0))
530 				{
531 					n2 = jp->file[1].fieldlen;
532 					continue;
533 				}
534 #if DEBUG_TRACE
535 sfprintf(sfstdout, "[2#%d:0,%lld,%lld]", __LINE__, lo, hi);
536 #endif
537 			}
538 			else if (cmp > 0)
539 			{
540 				if (same)
541 				{
542 					same = 0;
543 				next:
544 					if (n2 > jp->samesize)
545 					{
546 						jp->samesize = roundof(n2, 16);
547 						if (!(jp->same = newof(jp->same, char, jp->samesize, 0)))
548 						{
549 							error(ERROR_SYSTEM|2, "out of space");
550 							return -1;
551 						}
552 					}
553 					memcpy(jp->same, cp2, o2 = n2);
554 					if (!(cp2 = getrec(jp, 1, 0)))
555 						break;
556 					n2 = jp->file[1].fieldlen;
557 					if (n2 == o2 && *cp2 == *jp->same && !memcmp(cp2, jp->same, n2))
558 						goto next;
559 					continue;
560 				}
561 				if (hi >= 0)
562 				{
563 					if (sfseek(jp->file[1].iop, hi, SEEK_SET) != hi)
564 					{
565 						error(ERROR_SYSTEM|2, "%s: seek error", jp->file[1].name);
566 						return -1;
567 					}
568 					hi = -1;
569 				}
570 				else if ((jp->outmode & C_FILE2) && outrec(jp, 1) < 0)
571 					return -1;
572 				lo = -1;
573 				if (cp2 = getrec(jp, 1, 1))
574 				{
575 					n2 = jp->file[1].fieldlen;
576 					continue;
577 				}
578 #if DEBUG_TRACE
579 sfprintf(sfstdout, "[2#%d:0,%lld,%lld]", __LINE__, lo, hi);
580 #endif
581 			}
582 			else if (same)
583 			{
584 				same = 0;
585 				if (!(cp1 = getrec(jp, 0, 0)))
586 					break;
587 				n1 = jp->file[0].fieldlen;
588 				continue;
589 			}
590 			if (lo >= 0)
591 			{
592 				if ((hi = sfseek(jp->file[1].iop, (Sfoff_t)0, SEEK_CUR)) < 0 ||
593 				    (hi -= jp->file[1].reclen) < 0 ||
594 				    sfseek(jp->file[1].iop, lo, SEEK_SET) != lo ||
595 				    !(cp2 = getrec(jp, 1, 0)))
596 				{
597 					error(ERROR_SYSTEM|2, "%s: seek error", jp->file[1].name);
598 					return -1;
599 				}
600 				n2 = jp->file[1].fieldlen;
601 				lo = -1;
602 				if (jp->file[1].discard)
603 					sfseek(jp->file[1].iop, (Sfoff_t)-1, SEEK_SET);
604 			}
605 			else if (!cp2)
606 				break;
607 			else if ((jp->outmode & C_FILE1) && outrec(jp, -1) < 0)
608 				return -1;
609 			if (!(cp1 = getrec(jp, 0, 1)))
610 				break;
611 			n1 = jp->file[0].fieldlen;
612 		}
613 	}
614 #if DEBUG_TRACE
615 sfprintf(sfstdout, "[X#%d:?,%p,%p,%d%,%d,%d%s]", __LINE__, cp1, cp2, cmp, lo, hi, (jp->outmode & C_COMMON) ? ",COMMON" : "");
616 #endif
617 	if (cp2)
618 	{
619 		if (hi >= 0 &&
620 		    sfseek(jp->file[1].iop, (Sfoff_t)0, SEEK_CUR) < hi &&
621 		    sfseek(jp->file[1].iop, hi, SEEK_SET) != hi)
622 		{
623 			error(ERROR_SYSTEM|2, "%s: seek error", jp->file[1].name);
624 			return -1;
625 		}
626 #if DEBUG_TRACE
627 sfprintf(sfstdout, "[O#%d:%02o:%02o]", __LINE__, jp->ooutmode, jp->outmode);
628 #endif
629 		cp1 = (!cp1 && cmp && hi < 0 && !jp->file[1].hit && ((jp->ooutmode ^ C_ALL) <= 1 || jp->outmode == 2)) ? cp2 : getrec(jp, 1, 0);
630 		cmp = 1;
631 		n = 1;
632 	}
633 	else
634 	{
635 		cmp = -1;
636 		n = 0;
637 	}
638 #if DEBUG_TRACE
639 sfprintf(sfstdout, "[X#%d:%d,%p,%p,%d,%02o,%02o%s]", __LINE__, n, cp1, cp2, cmp, jp->ooutmode, jp->outmode, (jp->outmode & C_COMMON) ? ",COMMON" : "");
640 #endif
641 	if (!cp1 || !(jp->outmode & (1<<n)))
642 	{
643 		if (cp1 && jp->file[n].iop == sfstdin)
644 			sfseek(sfstdin, (Sfoff_t)0, SEEK_END);
645 		return 0;
646 	}
647 	if (outrec(jp, cmp) < 0)
648 		return -1;
649 	do
650 	{
651 		if (!getrec(jp, n, 1))
652 			return 0;
653 	} while (outrec(jp, cmp) >= 0);
654 	return -1;
655 }
656 
657 int
658 b_join(int argc, char** argv, void* context)
659 {
660 	register int		n;
661 	register char*		cp;
662 	register Join_t*	jp;
663 	char*			e;
664 
665 #if !DEBUG_TRACE
666 	cmdinit(argc, argv, context, ERROR_CATALOG, ERROR_NOTIFY);
667 #endif
668 	if (!(jp = init()))
669 		error(ERROR_system(1),"out of space");
670 	jp->context = context;
671 	for (;;)
672 	{
673 		switch (n = optget(argv, usage))
674 		{
675 		case 0:
676 			break;
677  		case 'j':
678 			/*
679 			 * check for obsolete "-j1 field" and "-j2 field"
680 			 */
681 
682 			if (opt_info.offset == 0)
683 			{
684 				cp = argv[opt_info.index - 1];
685 				for (n = strlen(cp) - 1; n > 0 && cp[n] != 'j'; n--);
686 				n = cp[n] == 'j';
687 			}
688 			else
689 				n = 0;
690 			if (n)
691 			{
692 				if (opt_info.num!=1 && opt_info.num!=2)
693 					error(2,"-jfileno field: fileno must be 1 or 2");
694 				n = '0' + opt_info.num;
695 				if (!(cp = argv[opt_info.index]))
696 				{
697 					argc = 0;
698 					break;
699 				}
700 				opt_info.num = strtol(cp, &e, 10);
701 				if (*e)
702 				{
703 					argc = 0;
704 					break;
705 				}
706 				opt_info.index++;
707 			}
708 			else
709 			{
710 				jp->file[0].field = (int)(opt_info.num-1);
711 				n = '2';
712 			}
713 			/*FALLTHROUGH*/
714  		case '1':
715 		case '2':
716 			if (opt_info.num <=0)
717 				error(2,"field number must positive");
718 			jp->file[n-'1'].field = (int)(opt_info.num-1);
719 			continue;
720 		case 'v':
721 			jp->outmode &= ~C_COMMON;
722 			/*FALLTHROUGH*/
723 		case 'a':
724 			if (opt_info.num!=1 && opt_info.num!=2)
725 				error(2,"%s: file number must be 1 or 2", opt_info.name);
726 			jp->outmode |= 1<<(opt_info.num-1);
727 			continue;
728 		case 'e':
729 			jp->nullfield = opt_info.arg;
730 			continue;
731 		case 'o':
732 			/* need to accept obsolescent command syntax */
733 			n = getolist(jp, opt_info.arg, argv+opt_info.index);
734 			opt_info.index += n;
735 			continue;
736 		case 't':
737 			jp->state[' '] = jp->state['\t'] = 0;
738 			n= *(unsigned char*)opt_info.arg;
739 			jp->state[n] = S_DELIM;
740 			jp->delim = n;
741 			continue;
742 		case 'i':
743 			jp->ignorecase = !opt_info.num;
744 			continue;
745 		case 'B':
746 			jp->buffered = !opt_info.num;
747 			continue;
748 		case ':':
749 			error(2, "%s", opt_info.arg);
750 			break;
751 		case '?':
752 			done(jp);
753 			error(ERROR_usage(2), "%s", opt_info.arg);
754 			break;
755 		}
756 		break;
757 	}
758 	argv += opt_info.index;
759 	argc -= opt_info.index;
760 	if (error_info.errors || argc!=2)
761 	{
762 		done(jp);
763 		error(ERROR_usage(2),"%s", optusage(NiL));
764 	}
765 	jp->ooutmode = jp->outmode;
766 	jp->file[0].name = cp = *argv++;
767 	if (streq(cp,"-"))
768 	{
769 		if (sfseek(sfstdin,(Sfoff_t)0,SEEK_CUR) < 0)
770 		{
771 			if (sfdcseekable(sfstdin))
772 				error(ERROR_warn(0),"%s: seek may fail",cp);
773 			else
774 				jp->file[0].discard = 1;
775 		}
776 		jp->file[0].iop = sfstdin;
777 	}
778 	else if (!(jp->file[0].iop = sfopen(NiL, cp, "r")))
779 	{
780 		done(jp);
781 		error(ERROR_system(1),"%s: cannot open",cp);
782 	}
783 	jp->file[1].name = cp = *argv;
784 	if (streq(cp,"-"))
785 	{
786 		if (sfseek(sfstdin,(Sfoff_t)0,SEEK_CUR) < 0)
787 		{
788 			if (sfdcseekable(sfstdin))
789 				error(ERROR_warn(0),"%s: seek may fail",cp);
790 			else
791 				jp->file[1].discard = 1;
792 		}
793 		jp->file[1].iop = sfstdin;
794 	}
795 	else if (!(jp->file[1].iop = sfopen(NiL, cp, "r")))
796 	{
797 		done(jp);
798 		error(ERROR_system(1),"%s: cannot open",cp);
799 	}
800 	if (jp->buffered)
801 	{
802 		sfsetbuf(jp->file[0].iop, jp->file[0].iop, SF_UNBOUND);
803 		sfsetbuf(jp->file[1].iop, jp->file[0].iop, SF_UNBOUND);
804 	}
805 	jp->state['\n'] = S_NL;
806 	jp->outfile = sfstdout;
807 	if (!jp->outlist)
808 		jp->nullfield = 0;
809 	if (join(jp) < 0)
810 	{
811 		done(jp);
812 		error(ERROR_system(1),"write error");
813 	}
814 	else if (jp->file[0].iop==sfstdin || jp->file[1].iop==sfstdin)
815 		sfseek(sfstdin,(Sfoff_t)0,SEEK_END);
816 	done(jp);
817 	return error_info.errors;
818 }
819