xref: /inferno-os/appl/cmd/diff.b (revision 37da2899f40661e3e9631e497da8dc59b971cbd0)
1implement Diff;
2
3#	diff - differential file comparison
4#
5#	Uses an algorithm due to Harold Stone, which finds
6#	a pair of longest identical subsequences in the two
7#	files.
8#
9#	The major goal is to generate the match vector J.
10#	J[i] is the index of the line in file1 corresponding
11#	to line i file0. J[i] = 0 if there is no
12#	such line in file1.
13#
14#	Lines are hashed so as to work in core. All potential
15#	matches are located by sorting the lines of each file
16#	on the hash (called value). In particular, this
17#	collects the equivalence classes in file1 together.
18#	Subroutine equiv replaces the value of each line in
19#	file0 by the index of the first element of its
20#	matching equivalence in (the reordered) file1.
21#	To save space equiv squeezes file1 into a single
22#	array member in which the equivalence classes
23#	are simply concatenated, except that their first
24#	members are flagged by changing sign.
25#
26#	Next the indices that point into member are unsorted into
27#	array class according to the original order of file0.
28#
29#	The cleverness lies in routine stone. This marches
30#	through the lines of file0, developing a vector klist
31#	of "k-candidates". At step i a k-candidate is a matched
32#	pair of lines x,y (x in file0 y in file1) such that
33#	there is a common subsequence of lenght k
34#	between the first i lines of file0 and the first y
35#	lines of file1, but there is no such subsequence for
36#	any smaller y. x is the earliest possible mate to y
37#	that occurs in such a subsequence.
38#
39#	Whenever any of the members of the equivalence class of
40#	lines in file1 matable to a line in file0 has serial number
41#	less than the y of some k-candidate, that k-candidate
42#	with the smallest such y is replaced. The new
43#	k-candidate is chained (via pred) to the current
44#	k-1 candidate so that the actual subsequence can
45#	be recovered. When a member has serial number greater
46#	that the y of all k-candidates, the klist is extended.
47#	At the end, the longest subsequence is pulled out
48#	and placed in the array J by unravel.
49#
50#	With J in hand, the matches there recorded are
51#	check'ed against reality to assure that no spurious
52#	matches have crept in due to hashing. If they have,
53#	they are broken, and "jackpot " is recorded--a harmless
54#	matter except that a true match for a spuriously
55#	mated line may now be unnecessarily reported as a change.
56#
57#	Much of the complexity of the program comes simply
58#	from trying to minimize core utilization and
59#	maximize the range of doable problems by dynamically
60#	allocating what is needed and reusing what is not.
61#	The core requirements for problems larger than somewhat
62#	are (in words) 2*length(file0) + length(file1) +
63#	3*(number of k-candidates installed),  typically about
64#	6n words for files of length n.
65#
66#
67
68include "sys.m";
69	sys: Sys;
70
71include "bufio.m";
72	bufio : Bufio;
73Iobuf : import bufio;
74
75include "draw.m";
76	draw: Draw;
77include "readdir.m";
78	readdir : Readdir;
79include "string.m";
80	str : String;
81include "arg.m";
82
83Diff : module
84{
85	init: fn(ctxt: ref Draw->Context, argv: list of string);
86};
87
88stderr: ref Sys->FD;
89
90mode : int;			# '\0', 'e', 'f', 'h'
91bflag : int;			# ignore multiple and trailing blanks
92rflag : int;			# recurse down directory trees
93mflag : int;			# pseudo flag: doing multiple files, one dir
94
95REG,
96BIN: con iota;
97
98HALFINT : con 16;
99Usage : con  "usage: diff [ -efbwr ] file1 ... file2";
100
101cand : adt {
102	x : int;
103	y : int;
104	pred : int;
105};
106
107line : adt {
108	serial : int;
109	value : int;
110};
111
112out : ref Iobuf;
113file := array[2] of array of line;
114sfile := array[2] of array of line;	# shortened by pruning common prefix and suffix
115slen := array[2] of int;
116ilen := array[2] of int;
117pref, suff, clen : int;			# length of prefix and suffix
118firstchange : int;
119clist : array of cand;			# merely a free storage pot for candidates
120J : array of int;			# will be overlaid on class
121ixold, ixnew : array of int;
122input := array[2] of ref Iobuf ;
123file1, file2 : string;
124tmpname := array[] of {"/tmp/diff1", "/tmp/diff2"};
125whichtmp : int;
126anychange := 0;
127
128init(nil: ref Draw->Context, args: list of string)
129{
130	sys = load Sys Sys->PATH;
131	stderr = sys->fildes(2);
132	draw = load Draw Draw->PATH;
133	bufio = load Bufio Bufio->PATH;
134	readdir = load Readdir Readdir->PATH;
135	str = load String String->PATH;
136	if (bufio==nil)
137		fatal(sys->sprint("cannot load %s: %r", Bufio->PATH));
138	if (readdir==nil)
139		fatal(sys->sprint("cannot load %s: %r", Readdir->PATH));
140	if (str==nil)
141		fatal(sys->sprint("cannot load %s: %r", String->PATH));
142	arg := load Arg Arg->PATH;
143	if (arg==nil)
144		fatal(sys->sprint("cannot load %s: %r", Arg->PATH));
145	fsb, tsb : Sys->Dir;
146	arg->init(args);
147	while((o := arg->opt()) != 0)
148		case o {
149		'e' or 'f' =>
150			mode = o;
151		'w' =>
152			bflag = 2;
153		'b' =>
154			bflag = 1;
155		'r' =>
156			rflag = 1;
157		'm' =>
158			mflag = 1;
159		* =>
160			fatal(Usage);
161		}
162	tmp := arg->argv();
163	arg = nil;
164	j := len tmp;
165	if (j < 2)
166		fatal(Usage);
167	arr := array[j] of string;
168	for(i:=0;i<j;i++){
169		arr[i]= hd tmp;
170		tmp = tl tmp;
171	}
172
173	(i,tsb)=sys->stat(arr[j-1]);
174	if (i == -1)
175		fatal(sys->sprint("can't stat %s: %r", arr[j-1]));
176	if (j > 2) {
177		if (!(tsb.qid.qtype&Sys->QTDIR))
178			fatal(Usage);
179		mflag = 1;
180	}
181	else {
182		(i,fsb)=sys->stat(arr[0]);
183		if (i == -1)
184			fatal(sys->sprint("can't stat %s: %r", arr[0]));
185		if ((fsb.qid.qtype&Sys->QTDIR) && (tsb.qid.qtype&Sys->QTDIR))
186			mflag = 1;
187	}
188	out=bufio->fopen(sys->fildes(1),Bufio->OWRITE);
189	for (i = 0; i < j-1; i++) {
190		diff(arr[i], arr[j-1], 0);
191		rmtmpfiles();
192	}
193	rmtmpfiles();
194	out.flush();
195	if (anychange)
196		raise "fail:some";
197}
198
199############################# diffreg from here ....
200
201# shellsort CACM #201
202
203sort(a : array of line, n : int)
204{
205	w : line;
206	j1:=0;
207	m := 0;
208	for (i := 1; i <= n; i *= 2)
209		m = 2*i - 1;
210	for (m /= 2; m != 0; m /= 2) {
211		for (j := 1; j <= n-m ; j++) {
212			ai:=j;
213			aim:=j+m;
214			do {
215				if (a[aim].value > a[ai].value ||
216				   a[aim].value == a[ai].value &&
217				   a[aim].serial > a[ai].serial)
218					break;
219				w = a[ai];
220				a[ai] = a[aim];
221				a[aim] = w;
222				aim=ai;
223				ai-=m;
224			} while (ai > 0 && aim >= ai);
225		}
226	}
227}
228
229unsort(f : array of line, l : int) : array of int
230{
231	i : int;
232	a := array[l+1] of int;
233	for(i=1;i<=l;i++)
234		a[f[i].serial] = f[i].value;
235	return a;
236}
237
238prune()
239{
240	for(pref=0;pref< ilen[0]&&pref< ilen[1]&&
241		file[0][pref+1].value==file[1][pref+1].value;
242		pref++ ) ;
243	for(suff=0;suff< ilen[0]-pref&&suff< ilen[1]-pref&&
244		file[0][ilen[0]-suff].value==file[1][ilen[1]-suff].value;
245		suff++) ;
246	for(j:=0;j<2;j++) {
247		sfile[j] = file[j][pref:];
248		slen[j]= ilen[j]-pref-suff;
249		for(i:=0;i<=slen[j];i++)
250			sfile[j][i].serial = i;
251	}
252}
253
254equiv(a: array of line, n:int , b: array of line, m: int, c : array of int)
255{
256	i := 1;
257	j := 1;
258	while(i<=n && j<=m) {
259		if(a[i].value < b[j].value)
260			a[i++].value = 0;
261		else if(a[i].value == b[j].value)
262			a[i++].value = j;
263		else
264			j++;
265	}
266	while(i <= n)
267		a[i++].value = 0;
268	b[m+1].value = 0; # huh ?
269	j = 1;
270	while(j <= m) {
271		c[j] = -b[j].serial;
272		while(b[j+1].value == b[j].value) {
273			j++;
274			c[j] = b[j].serial;
275		}
276		j++;
277	}
278	c[j] = -1;
279}
280
281newcand(x, y, pred : int) : int
282{
283	if (clen==len clist){
284		q := array[clen*2] of cand;
285		q[0:]=clist;
286		clist= array[clen*2] of cand;
287		clist[0:]=q;
288		q=nil;
289	}
290	clist[clen].x=x;
291	clist[clen].y=y;
292	clist[clen].pred=pred;
293	return clen++;
294}
295
296search(c : array of int, k,y : int) : int
297{
298	if(clist[c[k]].y < y)	# quick look for typical case
299		return k+1;
300	i := 0;
301	j := k+1;
302	while((l:=(i+j)/2) > i) {
303		t := clist[c[l]].y;
304		if(t > y)
305			j = l;
306		else if(t < y)
307			i = l;
308		else
309			return l;
310	}
311	return l+1;
312}
313
314stone(a : array of int ,n : int, b: array of int , c : array of int) : int
315{
316	oldc, oldl, tc, l ,y : int;
317	k := 0;
318	c[0] = newcand(0,0,0);
319	for(i:=1; i<=n; i++) {
320		j := a[i];
321		if(j==0)
322			continue;
323		y = -b[j];
324		oldl = 0;
325		oldc = c[0];
326		do {
327			if(y <= clist[oldc].y)
328				continue;
329			l = search(c, k, y);
330			if(l!=oldl+1)
331				oldc = c[l-1];
332			if(l<=k) {
333				if(clist[c[l]].y <= y)
334					continue;
335				tc = c[l];
336				c[l] = newcand(i,y,oldc);
337				oldc = tc;
338				oldl = l;
339			} else {
340				c[l] = newcand(i,y,oldc);
341				k++;
342				break;
343			}
344		} while((y=b[j+=1]) > 0);
345	}
346	return k;
347}
348
349unravel(p : int)
350{
351	for(i:=0; i<=ilen[0]; i++) {
352		if (i <= pref)
353			J[i] = i;
354		else if (i > ilen[0]-suff)
355			J[i] = i+ ilen[1]-ilen[0];
356		else
357			J[i] = 0;
358	}
359	for(q:=clist[p];q.y!=0;q=clist[q.pred])
360		J[q.x+pref] = q.y+pref;
361}
362
363output()
364{
365	i1: int;
366	m := ilen[0];
367	J[0] = 0;
368	J[m+1] = ilen[1]+1;
369	if (mode != 'e') {
370		for (i0 := 1; i0 <= m; i0 = i1+1) {
371			while (i0 <= m && J[i0] == J[i0-1]+1)
372				i0++;
373			j0 := J[i0-1]+1;
374			i1 = i0-1;
375			while (i1 < m && J[i1+1] == 0)
376				i1++;
377			j1 := J[i1+1]-1;
378			J[i1] = j1;
379			change(i0, i1, j0, j1);
380		}
381	}
382	else {
383		for (i0 := m; i0 >= 1; i0 = i1-1) {
384			while (i0 >= 1 && J[i0] == J[i0+1]-1 && J[i0])
385				i0--;
386			j0 := J[i0+1]-1;
387			i1 = i0+1;
388			while (i1 > 1 && J[i1-1] == 0)
389				i1--;
390			j1 := J[i1-1]+1;
391			J[i1] = j1;
392			change(i1 , i0, j1, j0);
393		}
394	}
395	if (m == 0)
396		change(1, 0, 1, ilen[1]);
397	out.flush();
398}
399
400diffreg(f,t : string)
401{
402	k : int;
403
404	(b0, b0type) := prepare(0, f);
405	if (b0==nil)
406		return;
407	(b1, b1type) := prepare(1, t);
408	if (b1==nil) {
409		b0=nil;
410		return;
411	}
412	if (b0type == BIN || b1type == BIN) {
413		if (cmp(b0, b1)) {
414			out.puts(sys->sprint("Binary files %s %s differ\n", f, t));
415			anychange = 1;
416		}
417		b0 = nil;
418		b1 = nil;
419		return;
420	}
421	clen=0;
422	prune();
423	file[0]=nil;
424	file[1]=nil;
425	sort(sfile[0],slen[0]);
426	sort(sfile[1],slen[1]);
427	member := array[slen[1]+2] of int;
428	equiv(sfile[0], slen[0],sfile[1],slen[1], member);
429	class:=unsort(sfile[0],slen[0]);
430	sfile[0]=nil;
431	sfile[1]=nil;
432	klist := array[slen[0]+2] of int;
433	clist = array[1] of cand;
434	k = stone(class, slen[0], member, klist);
435	J = array[ilen[0]+2] of int;
436	unravel(klist[k]);
437	clist=nil;
438	klist=nil;
439	class=nil;
440	member=nil;
441	ixold = array[ilen[0]+2] of int;
442	ixnew = array[ilen[1]+2] of int;
443
444	b0.seek(big 0, 0);
445	b1.seek(big 0, 0);
446	check(b0, b1);
447	output();
448	ixold=nil;
449	ixnew=nil;
450	b0=nil;
451	b1=nil;
452}
453
454######################## diffio starts here...
455
456
457# hashing has the effect of
458# arranging line in 7-bit bytes and then
459# summing 1-s complement in 16-bit hunks
460
461readhash(bp : ref Iobuf) : int
462{
463	sum := 1;
464	shift := 0;
465	buf := bp.gets('\n');
466	if (buf == nil)
467		return 0;
468	buf = buf[0:len buf -1];
469	p := 0;
470	case bflag {
471		# various types of white space handling
472		0 =>
473			while (p< len buf) {
474				sum += (buf[p] << (shift &= (HALFINT-1)));
475				p++;
476				shift += 7;
477			}
478		1 =>
479
480			 # coalesce multiple white-space
481
482			for (space := 0; p< len buf; p++) {
483				if (buf[p]==' ' || buf[p]=='\t') {
484					space++;
485					continue;
486				}
487				if (space) {
488					shift += 7;
489					space = 0;
490				}
491				sum +=  (buf[p] << (shift &= (HALFINT-1)));
492				p++;
493				shift += 7;
494			}
495		* =>
496
497			 # strip all white-space
498
499			while (p< len buf) {
500				if (buf[p]==' ' || buf[p]=='\t') {
501					p++;
502					continue;
503				}
504				sum +=  (buf[p] << (shift &= (HALFINT-1)));
505				p++;
506				shift += 7;
507			}
508	}
509	return sum;
510}
511
512prepare(i : int, arg : string) : (ref Iobuf, int)
513{
514	h : int;
515	bp := bufio->open(arg,Bufio->OREAD);
516	if (bp==nil) {
517		error(sys->sprint("cannot open %s: %r", arg));
518		return (nil, 0);
519	}
520	buf := array[1024] of byte;
521	n :=bp.read(buf, len buf);
522	str1 := string buf[0:n];
523	for (j:=0;j<len str1 -2;j++)
524		if (str1[j] == Sys->UTFerror)
525			return (bp, BIN);
526	bp.seek(big 0, Sys->SEEKSTART);
527	p := array[4] of line;
528	for (j = 0; h = readhash(bp); p[j].value = h){
529		j++;
530		if (j+3>=len p){
531			newp:=array[len p*2] of line;
532			newp[0:]=p[0:];
533			p=array[len p*2] of line;
534			p=newp;
535			newp=nil;
536		}
537	}
538	ilen[i]=j;
539	file[i] = p;
540	input[i] = bp;
541	if (i == 0) {
542		file1 = arg;
543		firstchange = 0;
544	}
545	else
546		file2 = arg;
547	return (bp, REG);
548}
549
550squishspace(buf : string) : string
551{
552	q:=0;
553	p:=0;
554	for (space := 0; q<len buf; q++) {
555		if (buf[q]==' ' || buf[q]=='\t') {
556			space++;
557			continue;
558		}
559		if (space && bflag == 1) {
560			buf[p] = ' ';
561			p++;
562			space = 0;
563		}
564		buf[p]=buf[q];
565		p++;
566	}
567	buf=buf[0:p];
568	return buf;
569}
570
571
572# need to fix up for unexpected EOF's
573
574ftell(b: ref Iobuf): int
575{
576	return int b.offset();
577}
578
579check(bf, bt : ref Iobuf)
580{
581	fbuf, tbuf : string;
582	f:=1;
583	t:=1;
584	ixold[0] = ixnew[0] = 0;
585	for (; f < ilen[0]; f++) {
586		fbuf = bf.gets('\n');
587		if (fbuf!=nil)
588			fbuf=fbuf[0:len fbuf -1];
589		ixold[f] = ftell(bf);
590		if (J[f] == 0)
591			continue;
592		tbuflen: int;
593		do {
594			tbuf = bt.gets('\n');
595			if (tbuf!=nil)
596				tbuf=tbuf[0:len tbuf -1];
597			tbuflen = len array of byte tbuf;
598			ixnew[t] = ftell(bt);
599		} while (t++ < J[f]);
600		if (bflag) {
601			fbuf = squishspace(fbuf);
602			tbuf = squishspace(tbuf);
603		}
604		if (len fbuf != len tbuf || fbuf!=tbuf)
605			J[f] = 0;
606	}
607	while (t < ilen[1]) {
608		tbuf = bt.gets('\n');
609		if (tbuf!=nil)
610			tbuf=tbuf[0:len tbuf -1];
611		ixnew[t] = ftell(bt);
612		t++;
613	}
614}
615
616range(a, b : int, separator : string)
617{
618	if (a>b)
619		out.puts(sys->sprint("%d", b));
620	else
621		out.puts(sys->sprint("%d", a));
622	if (a < b)
623		out.puts(sys->sprint("%s%d", separator, b));
624}
625
626fetch(f : array of int, a,b : int , bp : ref Iobuf, s : string)
627{
628	buf : string;
629	bp.seek(big f[a-1], 0);
630	while (a++ <= b) {
631		buf=bp.gets('\n');
632		out.puts(s);
633		out.puts(buf);
634	}
635}
636
637change(a, b, c, d : int)
638{
639	if (a > b && c > d)
640		return;
641	anychange = 1;
642	if (mflag && firstchange == 0) {
643		out.puts(sys->sprint( "diff %s %s\n", file1, file2));
644		firstchange = 1;
645	}
646	if (mode != 'f') {
647		range(a, b, ",");
648		if (a>b)
649			out.putc('a');
650		else if (c>d)
651			out.putc('d');
652		else
653			out.putc('c');
654		if (mode != 'e')
655			range(c, d, ",");
656	}
657	else {
658		if (a>b)
659			out.putc('a');
660		else if (c>d)
661			out.putc('d');
662		else
663			out.putc('c');
664		range(a, b, " ");
665	}
666	out.putc('\n');
667	if (mode == 0) {
668		fetch(ixold, a, b, input[0], "< ");
669		if (a <= b && c <= d)
670			out.puts("---\n");
671	}
672	if (mode==0)
673		fetch(ixnew, c, d, input[1], "> ");
674	else
675		fetch(ixnew, c, d, input[1], "");
676
677	if (mode != 0 && c <= d)
678		out.puts(".\n");
679}
680
681
682######################### diffdir starts here ......
683
684scandir(name : string) : array of string
685{
686	(db,nitems):= readdir->init(name,Readdir->NAME);
687	cp := array[nitems] of string;
688	for(i:=0;i<nitems;i++)
689		cp[i]=db[i].name;
690	return cp;
691}
692
693
694diffdir(f, t : string, level : int)
695{
696	df, dt : array of string;
697	fb, tb : string;
698	i:=0;
699	j:=0;
700	df = scandir(f);
701	dt = scandir(t);
702	while ((i<len df) || (j<len dt)) {
703		if ((j==len dt) || (i<len df && df[i] < dt[j])) {
704			if (mode == 0)
705				out.puts(sys->sprint("Only in %s: %s\n", f, df[i]));
706			i++;
707			continue;
708		}
709		if ((i==len df) || (j<len dt && df[i] > dt[j])) {
710			if (mode == 0)
711				out.puts(sys->sprint("Only in %s: %s\n", t, dt[j]));
712			j++;
713			continue;
714		}
715		fb=sys->sprint("%s/%s", f, df[i]);
716		tb=sys->sprint("%s/%s", t, dt[j]);
717		diff(fb, tb, level+1);
718		i++; j++;
719	}
720}
721
722cmp(b0, b1: ref Iobuf): int
723{
724	b0.seek(big 0, Sys->SEEKSTART);
725	b1.seek(big 0, Sys->SEEKSTART);
726	buf0 := array[1024] of byte;
727	buf1 := array[1024] of byte;
728	for (;;) {
729		n0 := b0.read(buf0, len buf0);
730		n1 := b1.read(buf1, len buf1);
731
732		if (n0 != n1)
733			return 1;
734
735		if (n0 == 0)
736			return 0;
737
738		for (i := 0; i < n0; i++)
739			if (buf0[i] != buf1[i])
740				return 1;
741	}
742}
743
744################## main from here.....
745
746REGULAR_FILE(s : Sys->Dir) : int
747{
748	# both pipes and networks contain non-zero-length files
749	# which are not seekable.
750	return (s.qid.qtype&Sys->QTDIR) == 0 &&
751		s.dtype != '|' &&
752		s.dtype != 'I';
753#		&& s.length > 0;	device files have zero length.
754}
755
756rmtmpfiles()
757{
758	while (whichtmp > 0) {
759		whichtmp--;
760		sys->remove(tmpname[whichtmp]);
761	}
762}
763
764mktmpfile(inputf : ref Sys->FD) : (string, Sys->Dir)
765{
766	i, j : int;
767	sb : Sys->Dir;
768	p : string;
769	buf := array[8192] of byte;
770
771	p = tmpname[whichtmp++];
772	fd := sys->create(p, Sys->OWRITE, 8r600);
773	if (fd == nil) {
774		error(sys->sprint("cannot create %s: %r", p));
775		return (nil, sb);
776	}
777	while ((i = sys->read(inputf, buf, len buf)) > 0) {
778		if ((i = sys->write(fd, buf, i)) < 0)
779			break;
780	}
781	(j,sb)=sys->fstat(fd);
782	if (i < 0 || j < 0) {
783		error(sys->sprint("cannot read/write %s: %r", p));
784		return (nil, sb);
785	}
786	return (p, sb);
787}
788
789
790statfile(file : string) : (string,Sys->Dir)
791{
792	(ret,sb):=sys->stat(file);
793	if (ret==-1) {
794		if (file == "-") {
795			 (ret,sb)= sys->fstat(sys->fildes(0));
796			if (ret == -1) {
797				error(sys->sprint("cannot stat %s: %r", file));
798				return (nil,sb);
799			}
800		}
801		(file, sb) = mktmpfile(sys->fildes(0));
802	}
803	else if (!REGULAR_FILE(sb) && !(sb.qid.qtype&Sys->QTDIR)) {
804		if ((i := sys->open(file, Sys->OREAD)) == nil) {
805			error(sys->sprint("cannot open %s: %r", file));
806			return (nil, sb);
807		}
808		(file, sb) = mktmpfile(i);
809	}
810	return (file,sb);
811}
812
813diff(f, t : string, level : int)
814{
815	fp,tp,p,rest,fb,tb : string;
816	fsb, tsb : Sys->Dir;
817	(fp,fsb) = statfile(f);
818	if (fp == nil)
819		return;
820	(tp,tsb) = statfile(t);
821	if (tp == nil)
822		return;
823	if ((fsb.qid.qtype&Sys->QTDIR) && (tsb.qid.qtype&Sys->QTDIR)) {
824		if (rflag || level == 0)
825			diffdir(fp, tp, level);
826		else
827			out.puts(sys->sprint("Common subdirectories: %s and %s\n", fp, tp));
828	}
829	else if (REGULAR_FILE(fsb) && REGULAR_FILE(tsb)){
830		diffreg(fp, tp);
831	} else {
832		if (!(fsb.qid.qtype&Sys->QTDIR)) {
833			(p,rest)=str->splitr(f,"/");
834			if (rest!=nil)
835				p = rest;
836			tb=sys->sprint("%s/%s", tp, p);
837			diffreg(fp, tb);
838		}
839		else {
840			(p,rest)=str->splitr(t,"/");
841			if (rest!=nil)
842				p = rest;
843			fb=sys->sprint("%s/%s", fp, p);
844			diffreg(fb, tp);
845		}
846	}
847}
848
849fatal(s: string)
850{
851	sys->fprint(stderr, "diff: %s\n", s);
852	raise "fail:error";
853}
854
855error(s: string)
856{
857	sys->fprint(stderr, "diff: %s\n", s);
858}
859