xref: /inferno-os/appl/cmd/sed.b (revision 3dcf5452967af1a7d07fa429b0c10e8e82cfd46d)
1implement Sed;
2
3#
4# partial sed implementation borrowed from plan9 sed.
5#
6
7include "sys.m";
8	sys: Sys;
9include "draw.m";
10include "arg.m";
11	arg: Arg;
12include "bufio.m";
13	bufio: Bufio;
14	Iobuf: import bufio;
15include "string.m";
16	str: String;
17include "regex.m";
18	regex: Regex;
19	Re: import regex;
20
21Sed : module {
22	init: fn(ctxt: ref Draw->Context, argv: list of string);
23};
24
25
26false, true: con iota;
27bool: type int;
28
29Addr: adt {
30	pick {
31	None =>
32	Dollar =>
33	Line =>
34		line: int;
35	Regex =>
36		re: Re;
37	}
38};
39
40Sedcom: adt {
41	command: fn(c: self ref Sedcom);
42	executable: fn(c: self ref Sedcom) : int;
43
44	ad1, ad2: ref Addr;
45	negfl: bool;
46	active: int;
47
48	pick {
49	S =>
50		gfl, pfl: int;
51		re: Re;
52		b: ref Iobuf;
53		rhs: string;
54	D or CD or P or Q or EQ or G or CG or H or CH or N or CN or X or CP or L=>
55	A or C or I =>
56		text: string;
57	R =>
58		filename: string;
59	W =>
60		b: ref Iobuf;
61	Y =>
62		map: list of (int, int);
63	B or T or Lab =>
64		lab: string;
65	}
66};
67
68dflag := false;
69nflag := false;
70gflag := false;
71sflag := 0;
72
73delflag := 0;
74dolflag := 0;
75fhead := 0;
76files: list of string;
77fout: ref Iobuf;
78infile: ref Iobuf;
79jflag := 0;
80lastregex:  Re;
81linebuf: string;
82filename := "";
83lnum := 0;
84peekc := 0;
85
86holdsp := "";
87patsp := "";
88
89cmds: list of ref Sedcom;
90appendlist: list of ref Sedcom;
91bufioflush: list of ref Iobuf;
92
93init(nil: ref Draw->Context, args: list of string)
94{
95	sys = load Sys Sys->PATH;
96
97	if ((arg = load Arg Arg->PATH) == nil)
98		fatal(sys->sprint("could not load %s: %r", Arg->PATH));
99
100	if ((bufio = load Bufio Bufio->PATH) == nil)
101		fatal(sys->sprint("could not load %s: %r", Bufio->PATH));
102
103	if ((str = load String String->PATH) == nil)
104		fatal(sys->sprint("could not load %s: %r", String->PATH));
105
106	if ((regex = load Regex Regex->PATH) == nil)
107		fatal(sys->sprint("could not load %s: %r", Regex->PATH));
108
109	arg->init(args);
110
111	compfl := 0;
112	while ((c := arg->opt()) != 0)
113		case c {
114		'n' =>
115			nflag = true;
116		'g' =>
117			gflag = true;
118		'e' =>
119			if ((s := arg->arg()) == nil)
120				usage();
121			filename = "";
122			cmds = compile(bufio->sopen(s + "\n"), cmds);
123			compfl = 1;
124		'f' => if ((filename = arg->arg()) == nil)
125				usage();
126			b := bufio->open(filename, bufio->OREAD);
127			if (b == nil)
128				fatal(sys->sprint("couldn't open '%s': %r", filename));
129			cmds = compile(b, cmds);
130			compfl = 1;
131		'd' =>
132			dflag = true;
133		* =>
134			usage();
135		}
136	args = arg->argv();
137	if (compfl == 0) {
138		if (len args == 0)
139			fatal("missing pattern");
140		filename = "";
141		cmds = compile(bufio->sopen(hd args + "\n"), cmds);
142		args = tl args;
143	}
144
145	# reverse command list, we could compile addresses here if required
146	l: list of ref Sedcom;
147	for (p := cmds; p != nil; p = tl p) {
148		l = hd p :: l;
149	}
150	cmds = l;
151
152	# add files to file list (and reverse to get in right order)
153	f: list of string;
154	if (len args == 0)
155		f = "" :: f;
156	else for (; len args != 0; args = tl args)
157		f = hd args :: f;
158	for (;f != nil; f = tl f)
159		files = hd f :: files;
160
161	if ((fout = bufio->fopen(sys->fildes(1), bufio->OWRITE)) == nil)
162		fatal(sys->sprint("couldn't buffer stdout: %r"));
163	bufioflush = fout :: bufioflush;
164	lnum = 0;
165	execute(cmds);
166	exits(nil);
167}
168
169depth := 0;
170maxdepth: con 20;
171cmdend := array [maxdepth] of string;
172cmdcnt := array [maxdepth] of int;
173
174compile(b: ref Iobuf, l: list of ref Sedcom) : list of ref Sedcom
175{
176	lnum = 1;
177
178nextline:
179	for (;;) {
180		err: int;
181		(err, linebuf) = getline(b);
182		if (err < 0)
183			break;
184
185		s := linebuf;
186
187		do {
188			rep: ref Sedcom;
189			ad1, ad2: ref Addr;
190			negfl := 0;
191
192			if (s != "")
193				s = str->drop(s, " \t;");
194
195			if (s == "" || s[0] == '#')
196				continue nextline;
197
198			# read addresses
199			(s, ad1) = address(s);
200			pick a := ad1 {
201			None =>
202				ad2 = ref Addr.None();
203			* =>
204				if (s != "" && (s[0] == ',' || s[0] == ';')) {
205					(s, ad2) = address(s[1:]);
206				}
207				else {
208					ad2 = ref Addr.None();
209				}
210			}
211
212			s = str->drop(s, " \t");
213
214			if (s != "" && str->in(s[0], "!")) {
215				negfl = true;
216				s = str->drop(s, "!");
217			}
218			s = str->drop(s, " \t");
219			if (s == "")
220				break;
221			c := s[0]; s = s[1:];
222
223			# mop up commands that got two addresses but only want one.
224			case c {
225				'a' or 'c' or 'q' or '=' or 'i' =>
226					if (tagof ad2 != tagof Addr.None)
227						fatal(sys->sprint("only one address allowed:  '%s'",
228						      linebuf));
229			}
230
231			case c {
232			* =>
233				fatal(sys->sprint("unrecognised command: '%s' (%c)",
234					linebuf, c));
235			'a' =>
236				if (s != "" && s[0] == '\\')
237					s = s[1:];
238				if (s == "" || s[0] != '\n')
239					fatal("unexpected characters in a command: " + s);
240				rep = ref Sedcom.A (ad1, ad2, negfl, 0, s[1:]);
241				s = "";
242			'c' =>
243				if (s != "" && s[0] == '\\')
244					s = s[1:];
245				if (s == "" || s[0] != '\n')
246					fatal("unexpected characters in c command: " + s);
247				rep = ref Sedcom.C (ad1, ad2, negfl, 0, s[1:]);
248				s = "";
249			'i' =>
250				if (s != "" && s[0] == '\\')
251					s = s[1:];
252				if (s == "" || s[0] != '\n')
253					fatal("unexpected characters in i command: " + s);
254				rep = ref Sedcom.I (ad1, ad2, negfl, 0, s[1:]);
255				s = "";
256			'r' =>
257				s = str->drop(s, " \t");
258				rep = ref Sedcom.R (ad1, ad2, negfl, 0, s);
259				s = "";
260			'w' =>
261				if (s != "")
262					s = str->drop(s, " \t");
263				if (s == "")
264					fatal("no filename in w command: " + linebuf);
265				bo := bufio->open(s, bufio->OWRITE);
266				if (bo == nil)
267					bo = bufio->create(s, bufio->OWRITE, 8r666);
268				if (bo == nil)
269					fatal(sys->sprint("can't create output file: '%s'", s));
270				bufioflush = bo :: bufioflush;
271				rep = ref Sedcom.W (ad1, ad2, negfl, 0, bo);
272				s = "";
273
274			'd' =>
275				rep = ref Sedcom.D (ad1, ad2, negfl, 0);
276			'D' =>
277				rep = ref Sedcom.CD (ad1, ad2, negfl, 0);
278			'p' =>
279				rep = ref Sedcom.P (ad1, ad2, negfl, 0);
280			'P' =>
281				rep = ref Sedcom.CP (ad1, ad2, negfl, 0);
282			'q' =>
283				rep = ref Sedcom.Q (ad1, ad2, negfl, 0);
284			'=' =>
285				rep = ref Sedcom.EQ (ad1, ad2, negfl, 0);
286			'g' =>
287				rep = ref Sedcom.G (ad1, ad2, negfl, 0);
288			'G' =>
289				rep = ref Sedcom.CG (ad1, ad2, negfl, 0);
290			'h' =>
291				rep = ref Sedcom.H (ad1, ad2, negfl, 0);
292			'H' =>
293				rep = ref Sedcom.CH (ad1, ad2, negfl, 0);
294			'n' =>
295				rep = ref Sedcom.N (ad1, ad2, negfl, 0);
296			'N' =>
297				rep = ref Sedcom.CN (ad1, ad2, negfl, 0);
298			'x' =>
299				rep = ref Sedcom.X (ad1, ad2, negfl, 0);
300			'l' =>
301				rep = ref Sedcom.L (ad1, ad2, negfl, 0);
302 			'y' =>
303				if (s == "")
304					fatal("expected args: " + linebuf);
305				seof := s[0:1];
306				s = s[1:];
307				if (s == "")
308					fatal("no lhs: " + linebuf);
309				(lhs, s2) := str->splitl(s, seof);
310				if (s2 == "")
311					fatal("no lhs terminator: " + linebuf);
312				s2 = s2[1:];
313				(rhs, s4) := str->splitl(s2, seof);
314				if (s4 == "")
315					fatal("no rhs: " + linebuf);
316				s = s4[1:];
317				if (len lhs != len rhs)
318					fatal("y command needs same length sets: " + linebuf);
319				map: list of (int, int);
320				for (i := 0; i < len lhs; i++)
321					map = (lhs[i], rhs[i]) :: map;
322				rep = ref Sedcom.Y (ad1, ad2, negfl, 0, map);
323			's' =>
324				seof := s[0:1];
325				re: Re;
326				(re, s) = recomp(s);
327				rhs: string;
328				(s, rhs) = compsub(seof + s);
329
330				gfl := gflag;
331				pfl := 0;
332
333				if (s != "" && s[0] == 'g') {
334					gfl = 1;
335					s = s[1:];
336				}
337				if (s != "" && s[0] == 'p') {
338					pfl = 1;
339					s = s[1:];
340				}
341				if (s != "" && s[0] == 'P') {
342					pfl = 2;
343					s = s[1:];
344				}
345
346				b: ref Iobuf = nil;
347				if (s != "" && s[0] == 'w') {
348					s = s[1:];
349					if (s != "")
350						s = str->drop(s, " \t");
351					if (s == "")
352						fatal("no filename in s with w: " + linebuf);
353					b = bufio->open(s, bufio->OWRITE);
354					if (b == nil)
355						b = bufio->create(s, bufio->OWRITE, 8r666);
356					if (b == nil)
357						fatal(sys->sprint("can't create output file: '%s'", s));
358					bufioflush = b :: bufioflush;
359					s = "";
360				}
361				rep = ref Sedcom.S (ad1, ad2, negfl, 0, gfl, pfl, re, b, rhs);
362			':' =>
363				if (s != "")
364					s = str->drop(s, " \t");
365				(lab, s1) := str->splitl(s, " \t;#");
366				s = s1;
367				if (lab == "")
368					fatal(sys->sprint("null label: '%s'", linebuf));
369				if (findlabel(lab))
370					fatal(sys->sprint("duplicate label: '%s'", lab));
371				rep = ref Sedcom.Lab (ad1, ad2, negfl, 0, lab);
372			'b' or 't' =>
373				if (s != "")
374					s = str->drop(s, " \t");
375				(lab, s1) := str->splitl(s, " \t;#");
376				s = s1;
377				if (c == 'b')
378					rep = ref Sedcom.B (ad1, ad2, negfl, 0, lab);
379				else
380					rep = ref Sedcom.T (ad1, ad2, negfl, 0, lab);
381			'{' =>
382				# replace { with branch to }.
383				lab := mklab(depth);
384				depth++;
385				rep = ref Sedcom.B (ad1, ad2, !negfl, 0, lab);
386				s = ";" + s;
387			'}' =>
388				if (tagof ad1 != tagof Addr.None)
389					fatal("did not expect address:" + linebuf);
390				if (--depth < 0)
391					fatal("too many }'s: " + linebuf);
392				lab := mklab(depth);
393				cmdcnt[depth]++;
394				rep = ref Sedcom.Lab ( ad1, ad2, negfl, 0, lab);
395				s = ";" + s;
396			}
397
398			l = rep :: l;
399		} while (s != nil && str->in(s[0], ";{}"));
400
401		if (s != nil)
402			fatal("leftover junk: " + s);
403	}
404	return l;
405}
406
407findlabel(lab: string) : bool
408{
409	for (l := cmds; l != nil; l = tl l)
410		pick x := hd l {
411		Lab =>
412			if (x.lab == lab)
413				return true;
414		}
415	return false;
416}
417
418mklab(depth: int): string
419{
420	return "_" + string cmdcnt[depth] + "_" + string depth;
421}
422
423Sedcom.command(c: self ref Sedcom)
424{
425	pick x := c {
426	S =>
427		m: bool;
428		(m, patsp) = substitute(x, patsp);
429		if (m) {
430			case x.pfl {
431			0 =>
432				;
433			1 =>
434				fout.puts(patsp + "\n");
435			* =>
436				l: string;
437				(l, patsp) = str->splitl(patsp, "\n");
438				fout.puts(l + "\n");
439				break;
440			}
441			if (x.b != nil)
442				x.b.puts(patsp + "\n");
443		}
444	P =>
445		fout.puts(patsp + "\n");
446	CP =>
447		(s, nil) := str->splitl(patsp, "\n");
448		fout.puts(s + "\n");
449	A =>
450		appendlist = c :: appendlist;
451	R =>
452		appendlist = c :: appendlist;
453	C =>
454		delflag++;
455		if (c.active == 1)
456			fout.puts(x.text + "\n");
457	I =>
458		fout.puts(x.text + "\n");
459	W =>
460		x.b.puts(patsp + "\n");
461	G =>
462		patsp = holdsp;
463	CG =>
464		patsp += holdsp;
465	H =>
466		holdsp = patsp;
467	CH =>
468		holdsp += patsp;
469	X =>
470		(holdsp, patsp) = (patsp, holdsp);
471	Y =>
472		# yes this is O(N²).
473		for (i := 0; i < len patsp; i++)
474			for (h := x.map; h != nil; h = tl h) {
475				(s, d) := hd h;
476				if (patsp[i] == s)
477					patsp[i] = d;
478			}
479	D =>
480		delflag++;
481	CD =>
482		# loose upto \n.
483		(s1, s2) := str->splitl(patsp, "\n");
484		if (s2 == nil)
485			patsp = s1;
486		else if (len s2 > 1)
487			patsp = s2[1:];
488		else
489			patsp = "";
490		jflag++;
491	Q =>
492		if (!nflag)
493			fout.puts(patsp + "\n");
494		arout();
495		exits(nil);
496	N =>
497		if (!nflag)
498			fout.puts(patsp + "\n");
499		arout();
500		n: int;
501		(patsp, n) = gline();
502		if (n < 0)
503			delflag++;
504	CN =>
505		arout();
506		(ns, n) := gline();
507		if (n < 0)
508			delflag++;
509		patsp += "\n" + ns;
510	EQ =>
511		fout.puts(sys->sprint("%d\n", lnum));
512	Lab =>
513		# labels don't do anything.
514	B =>
515		jflag = true;
516	T =>
517		if (sflag) {
518			sflag = false;
519			jflag = true;
520		}
521	L =>
522		col := 0;
523		cc := 0;
524		for (i := 0; i < len patsp; i++) {
525			s := "";
526			cc = patsp[i];
527			if (cc >= 16r20 && cc < 16r7F && cc != '\n')
528				s[len s] = cc;
529			else
530				s = trans(cc);
531			for (j := 0; j < len s; j++) {
532				fout.putc(s[j]);
533				if (col++ > 71) {
534					fout.puts("\\\n");
535					col = 0;
536				}
537			}
538		}
539		if (cc == ' ')
540			fout.puts("\\n");
541		fout.putc('\n');
542	* =>
543		fatal("unhandled command");
544	}
545}
546
547trans(ch: int) : string
548{
549	case ch {
550	'\b' =>
551		return "\\b";
552	'\n' =>
553		return "\\n";
554	'\r' =>
555		return "\\r";
556	'\t' =>
557		return "\\t";
558	'\\' =>
559		return "\\\\";
560	* =>
561		return sys->sprint("\\u%.4ux", ch);
562	}
563}
564
565getline(b: ref Iobuf) : (int, string)
566{
567	w : string;
568
569	lnum++;
570
571	while ((c := b.getc()) != bufio->EOF) {
572		r := c;
573		if (r == '\\') {
574			w[len w] = r;
575			if ((c = b.getc()) == bufio->EOF)
576				break;
577			r = c;
578		}
579		else if (r == '\n')
580			return (1, w);
581		w[len w] = r;
582	}
583	return (-1, w);
584}
585
586address(s: string) : (string, ref Addr)
587{
588	case s[0] {
589	'$' =>
590		return (s[1:], ref Addr.Dollar());
591	'/' =>
592		(r, s1) := recomp(s);
593		if (r == nil)
594			r = lastregex;
595		if (r == nil)
596			fatal("First RE in address may not be null");
597		return (s1, ref Addr.Regex(r));
598	'0' to '9' =>
599		(lno, ls) := str->toint(s, 10);
600		if (lno == 0)
601			fatal("line no 0 is illegal address");
602		return (ls, ref Addr.Line(lno));
603	* =>
604		return (s, ref Addr.None());
605	}
606}
607
608recomp(s :string) : (Re, string)
609{
610	expbuf := "";
611
612	seof := s[0]; s = s[1:];
613	if (s[0] == seof)
614		return (nil, s[1:]); # //
615
616	c := s[0]; s = s[1:];
617	do {
618		if (c == '\0' || c == '\n')
619			fatal("too much text: " + linebuf);
620		if (c == '\\') {
621			expbuf[len expbuf] = c;
622			c = s[0]; s = s[1:];
623			if (c == 'n')
624				c = '\n';
625		}
626		expbuf[len expbuf] = c;
627		c = s[0]; s = s[1:];
628	} while (c != seof);
629
630	(r, err) := regex->compile(expbuf, 1);
631	if (r == nil)
632		fatal(sys->sprint("%s '%s'", err, expbuf));
633
634	lastregex = r;
635
636	return (r, s);
637}
638
639compsub(s: string): (string, string)
640{
641	seof := s[0];
642	rhs := "";
643	for (i := 1; i < len s; i++) {
644		r := s[i];
645		if (r == seof)
646			break;
647		if (r == '\\') {
648			rhs[len rhs] = r;
649			if(++i >= len s)
650				break;
651			r = s[i];
652		}
653		rhs[len rhs] = r;
654	}
655	if (i >= len s)
656		fatal(sys->sprint("no closing %c in replacement text: %s", seof,  linebuf));
657	return (s[i+1:], rhs);
658}
659
660execute(l: list of ref Sedcom)
661{
662	for (;;) {
663		n: int;
664
665		(patsp, n) = gline();
666		if (n < 0)
667			break;
668
669cmdloop:
670		for (p := l; p != nil;) {
671			c := hd p;
672			if (!c.executable()) {
673				p = tl p;
674				continue;
675			}
676
677			c.command();
678
679			if (delflag)
680				break;
681			if (jflag) {
682				jflag = 0;
683				pick x := c {
684				B or T =>
685					if (p == nil)
686						break cmdloop;
687					for (p = l; p != nil; p = tl p) {
688						pick cc := hd p {
689						Lab =>
690							if (cc.lab == x.lab)
691								continue cmdloop;
692						}
693					}
694					break cmdloop; # unmatched branch => end of script
695				* =>
696					# don't branch.
697				}
698			}
699			else
700				p = tl p;
701		}
702		if (!nflag && !delflag)
703			fout.puts(patsp + "\n");
704		arout();
705		delflag = 0;
706	}
707}
708
709Sedcom.executable(c: self ref Sedcom) : int
710{
711	if (c.active) {
712		if (c.active == 1)
713			c.active = 2;
714		pick x := c.ad2 {
715		None =>
716			c.active = 0;
717		Dollar =>
718			return !c.negfl;
719		Line =>
720			if (lnum <= x.line) {
721				if (x.line == lnum)
722					c.active = 0;
723				return !c.negfl;
724			}
725			c.active = 0;
726			return c.negfl;
727		Regex =>
728			if (match(x.re, patsp))
729				c.active = false;
730			return !c.negfl;
731		}
732	}
733	pick x := c.ad1 {
734	None =>
735		return !c.negfl;
736	Dollar =>
737		if (dolflag)
738			return !c.negfl;
739	Line =>
740		if (x.line == lnum) {
741			c.active = 1;
742			return !c.negfl;
743		}
744	Regex =>
745		if (match(x.re, patsp)) {
746			c.active = 1;
747			return !c.negfl;
748		}
749	}
750	return c.negfl;
751}
752
753arout()
754{
755	a: list of ref Sedcom;
756
757	while (appendlist != nil) {
758		a = hd appendlist :: a;
759		appendlist = tl appendlist;
760	}
761
762	for (; a != nil; a = tl a)
763		pick x := hd a {
764		A =>
765			fout.puts(x.text + "\n");
766		R =>
767			if ((b := bufio->open(x.filename, bufio->OREAD)) == nil)
768				fatal(sys->sprint("couldn't open '%s'", x.filename));
769			while ((c := b.getc()) != bufio->EOF)
770				fout.putc(c);
771			b.close();
772		* =>
773			fatal("unexpected command on appendlist");
774		}
775}
776
777match(re: Re, s: string) : bool
778{
779	return re != nil && regex->execute(re, s) != nil;
780}
781
782substitute(c: ref Sedcom.S, s: string) : (bool, string)
783{
784	if (!match(c.re, s))
785		return (false, s);
786	sflag = true;
787	start := 0;
788
789	# Beware of infinite loops: 's/$/i/g', 's/a/aa/g', 's/^/a/g'
790	do {
791		se := (start, len s);
792		if ((m := regex->executese(c.re, s, se, true, true)) == nil)
793			break;
794		(l, r) := m[0];
795		rep := "";
796		for (i := 0; i < len c.rhs; i++){
797			if (c.rhs[i] != '\\'  || i+1 == len c.rhs){
798				if (c.rhs[i] == '&')
799					rep += s[l: r];
800				else
801					rep[len rep] = c.rhs[i];
802			}else {
803				i++;
804				case c.rhs[i] {
805				'0' to '9' =>
806					n := c.rhs[i] - '0';
807					# elide if too big
808					if (n < len m) {
809						(beg, end) := m[n];
810						rep += s[beg:end];
811					}
812				'n' =>
813					rep[len rep] = '\n';
814				* =>
815					rep[len rep] = c.rhs[i];
816				}
817			}
818		}
819		s = s[0:l] + rep + s[r:];
820		start = l + len rep;
821		if(r == l)
822			start++;
823	} while (c.gfl);
824	return (true, s);
825}
826
827gline() : (string, int)
828{
829	if (infile == nil && opendatafile() < 0)
830		return (nil, -1);
831
832	sflag = false;
833	lnum++;
834
835	s := "";
836	do {
837		c := peekc;
838		if (c == 0)
839			c = infile.getc();
840		for (; c != bufio->EOF; c = infile.getc()) {
841			if (c == '\n') {
842				if ((peekc = infile.getc()) == bufio->EOF)
843					if (fhead == 0)
844						dolflag = 1;
845				return (s, 1);
846			}
847			s[len s] = c;
848		}
849		if (len s != 0) {
850			peekc = bufio->EOF;
851			if (fhead == 0)
852				dolflag = 1;
853			return (s, 1);
854		}
855		peekc = 0;
856		infile = nil;
857	} while (opendatafile() > 0);
858	infile = nil;
859	return (nil, -1);
860}
861
862opendatafile() : int
863{
864	if (files == nil)
865		return -1;
866	if (hd files != nil) {
867		if ((infile = bufio->open(hd files, bufio->OREAD)) == nil)
868			fatal(sys->sprint("can't open '%s'", hd files));
869	}
870	else if ((infile = bufio->fopen(sys->fildes(0), bufio->OREAD)) == nil)
871		fatal("can't buffer stdin");
872
873	files = tl files;
874	return 1;
875}
876
877dbg(s: string)
878{
879	if (dflag)
880		sys->print("dbg: %s\n", s);
881}
882
883usage()
884{
885	sys->fprint(stderr(), "usage: %s [-ngd] [-e expr] [-f file] [expr] [file...]\n",
886		arg->progname());
887	exits("usage");
888}
889
890fatal(s: string)
891{
892	f := filename;
893	if (f == nil)
894		f = "<stdin>";
895	sys->fprint(stderr(), "%s:%d %s\n", f, lnum, s);
896	exits("error");
897}
898
899exits(e: string)
900{
901	for(; bufioflush != nil; bufioflush = tl bufioflush)
902		(hd bufioflush).flush();
903	if (e != nil)
904		raise "fail:" + e;
905	exit;
906}
907
908stderr() : ref Sys->FD
909{
910	return sys->fildes(2);
911}
912