date and time created 80/07/31 23:00:15 by mark
[unix-history] / usr / src / usr.bin / ex / ex_io.c
CommitLineData
544cd46c
MH
1/* Copyright (c) 1979 Regents of the University of California */
2#include "ex.h"
3#include "ex_argv.h"
4#include "ex_temp.h"
5#include "ex_tty.h"
6#include "ex_vis.h"
7
8/*
9 * File input/output, unix escapes, source, filtering preserve and recover
10 */
11
12/*
13 * Following remember where . was in the previous file for return
14 * on file switching.
15 */
16short altdot;
17short oldadot;
18bool wasalt;
19
20long cntch; /* Count of characters on unit io */
21short cntln; /* Count of lines " */
22long cntnull; /* Count of nulls " */
23long cntodd; /* Count of non-ascii characters " */
24
25/*
26 * Parse file name for command encoded by comm.
27 * If comm is E then command is doomed and we are
28 * parsing just so user won't have to retype the name.
29 */
30filename(comm)
31 int comm;
32{
33 register int c = comm, d;
34 register int i;
35
36 d = getchar();
37 if (endcmd(d)) {
38 if (savedfile[0] == 0 && comm != 'f')
39 error("No file|No current filename");
40 CP(file, savedfile);
41 wasalt = 0;
42 oldadot = altdot;
43 if (d == EOF)
44 ungetchar(d);
45 } else {
46 ungetchar(d);
47 getone();
48 eol();
49 if (savedfile[0] == 0 && c != 'E' && c != 'e') {
50 c = 'e';
51 edited = 0;
52 }
53 wasalt = strcmp(file, altfile) == 0;
54 oldadot = altdot;
55 switch (c) {
56
57 case 'f':
58 edited = 0;
59 /* fall into ... */
60
61 case 'e':
62 if (savedfile[0]) {
63 altdot = lineDOT();
64 CP(altfile, savedfile);
65 }
66 CP(savedfile, file);
67 break;
68
69 default:
70 if (file[0]) {
71 if (c != 'E')
72 altdot = lineDOT();
73 CP(altfile, file);
74 }
75 break;
76 }
77 }
78 if (hush && comm != 'f' || comm == 'E')
79 return;
80 if (file[0] != 0) {
81 lprintf("\"%s\"", file);
82 if (comm == 'f') {
83 if (!edited)
84 printf(" [Not edited]");
85 if (tchng)
86 printf(" [Modified]");
87 }
88 flush();
89 } else
90 printf("No file ");
91 if (comm == 'f') {
92 if (!(i = lineDOL()))
93 i++;
94 printf(" line %d of %d --%ld%%--", lineDOT(), lineDOL(),
95 (long) 100 * lineDOT() / i);
96 }
97}
98
99/*
100 * Get the argument words for a command into genbuf
101 * expanding # and %.
102 */
103getargs()
104{
105 register int c;
106 register char *cp, *fp;
107 static char fpatbuf[32]; /* hence limit on :next +/pat */
108
109 pastwh();
110 if (peekchar() == '+') {
111 for (cp = fpatbuf;;) {
112 c = *cp++ = getchar();
113 if (cp >= &fpatbuf[sizeof(fpatbuf)])
114 error("Pattern too long");
115 if (c == '\\' && isspace(peekchar()))
116 c = getchar();
117 if (c == EOF || isspace(c)) {
118 ungetchar(c);
119 *--cp = 0;
120 firstpat = &fpatbuf[1];
121 break;
122 }
123 }
124 }
125 if (skipend())
126 return (0);
127 CP(genbuf, "echo "); cp = &genbuf[5];
128 for (;;) {
129 c = getchar();
130 if (endcmd(c)) {
131 ungetchar(c);
132 break;
133 }
134 switch (c) {
135
136 case '\\':
137 if (any(peekchar(), "#%"))
138 c = getchar();
139 /* fall into... */
140
141 default:
142 if (cp > &genbuf[LBSIZE - 2])
143flong:
144 error("Argument buffer overflow");
145 *cp++ = c;
146 break;
147
148 case '#':
149 fp = altfile;
150 if (*fp == 0)
151 error("No alternate filename@to substitute for #");
152 goto filexp;
153
154 case '%':
155 fp = savedfile;
156 if (*fp == 0)
157 error("No current filename@to substitute for %%");
158filexp:
159 while (*fp) {
160 if (cp > &genbuf[LBSIZE - 2])
161 goto flong;
162 *cp++ = *fp++;
163 }
164 break;
165 }
166 }
167 *cp = 0;
168 return (1);
169}
170
171/*
172 * Glob the argument words in genbuf, or if no globbing
173 * is implied, just split them up directly.
174 */
175glob(gp)
176 struct glob *gp;
177{
178 int pvec[2];
179 register char **argv = gp->argv;
180 register char *cp = gp->argspac;
181 register int c;
182 char ch;
183 int nleft = NCARGS;
184
185 gp->argc0 = 0;
186 if (gscan() == 0) {
187 register char *v = genbuf + 5; /* strlen("echo ") */
188
189 for (;;) {
190 while (isspace(*v))
191 v++;
192 if (!*v)
193 break;
194 *argv++ = cp;
195 while (*v && !isspace(*v))
196 *cp++ = *v++;
197 *cp++ = 0;
198 gp->argc0++;
199 }
200 *argv = 0;
201 return;
202 }
203 if (pipe(pvec) < 0)
204 error("Can't make pipe to glob");
205 pid = fork();
206 io = pvec[0];
207 if (pid < 0) {
208 close(pvec[1]);
209 error("Can't fork to do glob");
210 }
211 if (pid == 0) {
212 int oerrno;
213
214 close(1);
215 dup(pvec[1]);
216 close(pvec[0]);
217 execl(svalue(SHELL), "sh", "-c", genbuf, 0);
218 oerrno = errno; close(1); dup(2); errno = oerrno;
219 filioerr(svalue(SHELL));
220 }
221 close(pvec[1]);
222 do {
223 *argv = cp;
224 for (;;) {
225 if (read(io, &ch, 1) != 1) {
226 close(io);
227 c = -1;
228 } else
229 c = ch & TRIM;
230 if (c <= 0 || isspace(c))
231 break;
232 *cp++ = c;
233 if (--nleft <= 0)
234 error("Arg list too long");
235 }
236 if (cp != *argv) {
237 --nleft;
238 *cp++ = 0;
239 gp->argc0++;
240 if (gp->argc0 >= NARGS)
241 error("Arg list too long");
242 argv++;
243 }
244 } while (c >= 0);
245 waitfor();
246 if (gp->argc0 == 0)
247 error(NOSTR);
248}
249
250/*
251 * Scan genbuf for shell metacharacters.
252 * Set is union of v7 shell and csh metas.
253 */
254gscan()
255{
256 register char *cp;
257
258 for (cp = genbuf; *cp; cp++)
259 if (any(*cp, "~{[*?$`'\"\\"))
260 return (1);
261 return (0);
262}
263
264/*
265 * Parse one filename into file.
266 */
267getone()
268{
269 register char *str;
270 struct glob G;
271
272 if (getargs() == 0)
273 error("Missing filename");
274 glob(&G);
275 if (G.argc0 > 1)
276 error("Ambiguous|Too many file names");
277 str = G.argv[G.argc0 - 1];
278 if (strlen(str) > FNSIZE - 4)
279 error("Filename too long");
280samef:
281 CP(file, str);
282}
283
284/*
285 * Read a file from the world.
286 * C is command, 'e' if this really an edit (or a recover).
287 */
288rop(c)
289 int c;
290{
291 register int i;
292 struct stat stbuf;
293 short magic;
294
295 io = open(file, 0);
296 if (io < 0) {
297 if (c == 'e' && errno == ENOENT)
298 edited++;
299 syserror();
300 }
301 if (fstat(io, &stbuf))
302 syserror();
303 switch (stbuf.st_mode & S_IFMT) {
304
305 case S_IFBLK:
306 error(" Block special file");
307
308 case S_IFCHR:
309 if (isatty(io))
310 error(" Teletype");
311 if (samei(&stbuf, "/dev/null"))
312 break;
313 error(" Character special file");
314
315 case S_IFDIR:
316 error(" Directory");
317
318 case S_IFREG:
319 i = read(io, (char *) &magic, sizeof(magic));
320 lseek(io, 0l, 0);
321 if (i != sizeof(magic))
322 break;
323 switch (magic) {
324
325 case 0405:
326 case 0407:
327 case 0410:
328 case 0411:
329 error(" Executable");
330
331 case 0177545:
332 case 0177555:
333 error(" Archive");
334
335 default:
336 if (magic & 0100200)
337 error(" Non-ascii file");
338 break;
339 }
340 }
341 if (c == 'r')
342 setdot();
343 else
344 setall();
345 if (inopen && c == 'r')
346 undap1 = undap2 = dot + 1;
347 rop2();
348 rop3(c);
349}
350
351rop2()
352{
353
354 deletenone();
355 clrstats();
356 ignore(append(getfile, addr2));
357}
358
359rop3(c)
360 int c;
361{
362
363 if (iostats() == 0 && c == 'e')
364 edited++;
365 if (c == 'e') {
366 if (wasalt || firstpat) {
367 register line *addr = zero + oldadot;
368
369 if (addr > dol)
370 addr = dol;
371 if (firstpat) {
372 globp = (*firstpat) ? firstpat : "$";
373 commands(1,1);
374 firstpat = 0;
375 } else if (addr >= one) {
376 if (inopen)
377 dot = addr;
378 markpr(addr);
379 } else
380 goto other;
381 } else
382other:
383 if (dol > zero) {
384 if (inopen)
385 dot = one;
386 markpr(one);
387 }
388 undkind = UNDNONE;
389 if (inopen) {
390 vcline = 0;
391 vreplace(0, LINES, lineDOL());
392 }
393 }
394 if (laste) {
395 laste = 0;
396 sync();
397 }
398}
399
400/*
401 * Are these two really the same inode?
402 */
403samei(sp, cp)
404 struct stat *sp;
405 char *cp;
406{
407 struct stat stb;
408
409 if (stat(cp, &stb) < 0 || sp->st_dev != stb.st_dev)
410 return (0);
411 return (sp->st_ino == stb.st_ino);
412}
413
414/* Returns from edited() */
415#define EDF 0 /* Edited file */
416#define NOTEDF -1 /* Not edited file */
417#define PARTBUF 1 /* Write of partial buffer to Edited file */
418
419/*
420 * Write a file.
421 */
422wop(dofname)
423bool dofname; /* if 1 call filename, else use savedfile */
424{
425 register int c, exclam, nonexist;
426 line *saddr1, *saddr2;
427 struct stat stbuf;
428
429 c = 0;
430 exclam = 0;
431 if (dofname) {
432 if (peekchar() == '!')
433 exclam++, ignchar();
434 ignore(skipwh());
435 while (peekchar() == '>')
436 ignchar(), c++, ignore(skipwh());
437 if (c != 0 && c != 2)
438 error("Write forms are 'w' and 'w>>'");
439 filename('w');
440 } else {
441 saddr1=addr1;
442 saddr2=addr2;
443 addr1=one;
444 addr2=dol;
445 CP(file, savedfile);
446 if (inopen) {
447 vclrech(0);
448 splitw++;
449 }
450 lprintf("\"%s\"", file);
451 }
452 nonexist = stat(file, &stbuf);
453 switch (c) {
454
455 case 0:
456 if (!exclam && !value(WRITEANY)) switch (edfile()) {
457
458 case NOTEDF:
459 if (nonexist)
460 break;
461 if ((stbuf.st_mode & S_IFMT) == S_IFCHR) {
462 if (samei(&stbuf, "/dev/null"))
463 break;
464 if (samei(&stbuf, "/dev/tty"))
465 break;
466 }
467 io = open(file, 1);
468 if (io < 0)
469 syserror();
470 if (!isatty(io))
471 serror(" File exists| File exists - use \"w! %s\" to overwrite", file);
472 close(io);
473 break;
474
475 case PARTBUF:
476 error(" Use \"w!\" to write partial buffer");
477 }
478cre:
479 synctmp();
480#ifdef V6
481 io = creat(file, 0644);
482#else
483 io = creat(file, 0666);
484#endif
485 if (io < 0)
486 syserror();
487 if (hush == 0)
488 if (nonexist)
489 printf(" [New file]");
490 else if (value(WRITEANY) && edfile() != EDF)
491 printf(" [Existing file]");
492 break;
493
494 case 2:
495 io = open(file, 1);
496 if (io < 0) {
497 if (exclam || value(WRITEANY))
498 goto cre;
499 syserror();
500 }
501 lseek(io, 0l, 2);
502 break;
503 }
504 putfile();
505 ignore(iostats());
506 if (c != 2 && addr1 == one && addr2 == dol) {
507 if (eq(file, savedfile))
508 edited = 1;
509 sync();
510 }
511 if (!dofname) {
512 addr1 = saddr1;
513 addr2 = saddr2;
514 }
515}
516
517/*
518 * Is file the edited file?
519 * Work here is that it is not considered edited
520 * if this is a partial buffer, and distinguish
521 * all cases.
522 */
523edfile()
524{
525
526 if (!edited || !eq(file, savedfile))
527 return (NOTEDF);
528 return (addr1 == one && addr2 == dol ? EDF : PARTBUF);
529}
530
531/*
532 * First part of a shell escape,
533 * parse the line, expanding # and % and ! and printing if implied.
534 */
535unix0(warn)
536 bool warn;
537{
538 register char *up, *fp;
539 register short c;
540 char printub, puxb[UXBSIZE + sizeof (int)];
541
542 printub = 0;
543 CP(puxb, uxb);
544 c = getchar();
545 if (c == '\n' || c == EOF)
546 error("Incomplete shell escape command@- use 'shell' to get a shell");
547 up = uxb;
548 do {
549 switch (c) {
550
551 case '\\':
552 if (any(peekchar(), "%#!"))
553 c = getchar();
554 default:
555 if (up >= &uxb[UXBSIZE]) {
556tunix:
557 uxb[0] = 0;
558 error("Command too long");
559 }
560 *up++ = c;
561 break;
562
563 case '!':
564 fp = puxb;
565 if (*fp == 0) {
566 uxb[0] = 0;
567 error("No previous command@to substitute for !");
568 }
569 printub++;
570 while (*fp) {
571 if (up >= &uxb[UXBSIZE])
572 goto tunix;
573 *up++ = *fp++;
574 }
575 break;
576
577 case '#':
578 fp = altfile;
579 if (*fp == 0) {
580 uxb[0] = 0;
581 error("No alternate filename@to substitute for #");
582 }
583 goto uexp;
584
585 case '%':
586 fp = savedfile;
587 if (*fp == 0) {
588 uxb[0] = 0;
589 error("No filename@to substitute for %%");
590 }
591uexp:
592 printub++;
593 while (*fp) {
594 if (up >= &uxb[UXBSIZE])
595 goto tunix;
596 *up++ = *fp++ | QUOTE;
597 }
598 break;
599 }
600 c = getchar();
601 } while (c == '|' || !endcmd(c));
602 if (c == EOF)
603 ungetchar(c);
604 *up = 0;
605 if (!inopen)
606 resetflav();
607 if (warn)
608 ckaw();
609 if (warn && hush == 0 && chng && xchng != chng && value(WARN) && dol > zero) {
610 xchng = chng;
611 vnfl();
612 printf(mesg("[No write]|[No write since last change]"));
613 noonl();
614 flush();
615 } else
616 warn = 0;
617 if (printub) {
618 if (uxb[0] == 0)
619 error("No previous command@to repeat");
620 if (inopen) {
621 splitw++;
622 vclean();
623 vgoto(WECHO, 0);
624 }
625 if (warn)
626 vnfl();
627 if (hush == 0)
628 lprintf("!%s", uxb);
629 if (inopen) {
630 vclreol();
631 vgoto(WECHO, 0);
632 } else
633 putnl();
634 flush();
635 }
636}
637
638/*
639 * Do the real work for execution of a shell escape.
640 * Mode is like the number passed to open system calls
641 * and indicates filtering. If input is implied, newstdin
642 * must have been setup already.
643 */
644unixex(opt, up, newstdin, mode)
645 char *opt, *up;
646 int newstdin, mode;
647{
648 int pvec[2], f;
649
650 signal(SIGINT, SIG_IGN);
651 if (inopen)
652 f = setty(normf);
653 if ((mode & 1) && pipe(pvec) < 0) {
654 /* Newstdin should be io so it will be closed */
655 if (inopen)
656 setty(f);
657 error("Can't make pipe for filter");
658 }
659#ifndef VFORK
660 pid = fork();
661#else
662 pid = vfork();
663#endif
664 if (pid < 0) {
665 if (mode & 1) {
666 close(pvec[0]);
667 close(pvec[1]);
668 }
669 setrupt();
670 error("No more processes");
671 }
672 if (pid == 0) {
673 if (mode & 2) {
674 close(0);
675 dup(newstdin);
676 close(newstdin);
677 }
678 if (mode & 1) {
679 close(pvec[0]);
680 close(1);
681 dup(pvec[1]);
682 if (inopen) {
683 close(2);
684 dup(1);
685 }
686 close(pvec[1]);
687 }
688 if (io)
689 close(io);
690 if (tfile)
691 close(tfile);
692 close(erfile);
693 signal(SIGHUP, oldhup);
694 signal(SIGQUIT, oldquit);
695 if (ruptible)
696 signal(SIGINT, SIG_DFL);
697 execl(svalue(SHELL), "sh", opt, up, (char *) 0);
698 printf("No %s!\n", svalue(SHELL));
699 error(NOSTR);
700 }
701 if (mode & 1) {
702 io = pvec[0];
703 close(pvec[1]);
704 }
705 if (newstdin)
706 close(newstdin);
707 return (f);
708}
709
710/*
711 * Wait for the command to complete.
712 * F is for restoration of tty mode if from open/visual.
713 * C flags suppression of printing.
714 */
715unixwt(c, f)
716 bool c;
717 int f;
718{
719
720 waitfor();
721 if (inopen)
722 setty(f);
723 setrupt();
724 if (!inopen && c && hush == 0) {
725 printf("!\n");
726 flush();
727 termreset();
728 gettmode();
729 }
730}
731
732/*
733 * Setup a pipeline for the filtration implied by mode
734 * which is like a open number. If input is required to
735 * the filter, then a child editor is created to write it.
736 * If output is catch it from io which is created by unixex.
737 */
738filter(mode)
739 register int mode;
740{
741 static int pvec[2];
742 register int f;
743 register int lines = lineDOL();
744
745 mode++;
746 if (mode & 2) {
747 signal(SIGINT, SIG_IGN);
748 if (pipe(pvec) < 0)
749 error("Can't make pipe");
750 pid = fork();
751 io = pvec[0];
752 if (pid < 0) {
753 setrupt();
754 close(pvec[1]);
755 error("No more processes");
756 }
757 if (pid == 0) {
758 setrupt();
759 io = pvec[1];
760 close(pvec[0]);
761 putfile();
762 exit(0);
763 }
764 close(pvec[1]);
765 io = pvec[0];
766 setrupt();
767 }
768 f = unixex("-c", uxb, (mode & 2) ? pvec[0] : 0, mode);
769 if (mode == 3) {
770 delete(0);
771 addr2 = addr1 - 1;
772 }
773 if (mode & 1) {
774 undap1 = undap2 = addr2+1;
775 ignore(append(getfile, addr2));
776 }
777 close(io);
778 io = -1;
779 unixwt(!inopen, f);
780 netchHAD(lines);
781}
782
783/*
784 * Set up to do a recover, getting io to be a pipe from
785 * the recover process.
786 */
787recover()
788{
789 static int pvec[2];
790
791 if (pipe(pvec) < 0)
792 error(" Can't make pipe for recovery");
793 pid = fork();
794 io = pvec[0];
795 if (pid < 0) {
796 close(pvec[1]);
797 error(" Can't fork to execute recovery");
798 }
799 if (pid == 0) {
800 close(2);
801 dup(1);
802 close(1);
803 dup(pvec[1]);
804 close(pvec[1]);
805 execl(EXRECOVER, "exrecover", svalue(DIRECTORY), file, (char *) 0);
806 close(1);
807 dup(2);
808 error(" No recovery routine");
809 }
810 close(pvec[1]);
811}
812
813/*
814 * Wait for the process (pid an external) to complete.
815 */
816waitfor()
817{
818
819 do
820 rpid = wait(&status);
821 while (rpid != pid && rpid != -1);
822 status = (status >> 8) & 0377;
823}
824
825/*
826 * The end of a recover operation. If the process
827 * exits non-zero, force not edited; otherwise force
828 * a write.
829 */
830revocer()
831{
832
833 waitfor();
834 if (pid == rpid && status != 0)
835 edited = 0;
836 else
837 change();
838}
839
840/*
841 * Extract the next line from the io stream.
842 */
843static char *nextip;
844
845getfile()
846{
847 register short c;
848 register char *lp, *fp;
849
850 lp = linebuf;
851 fp = nextip;
852 do {
853 if (--ninbuf < 0) {
854 ninbuf = read(io, genbuf, LBSIZE) - 1;
855 if (ninbuf < 0) {
856 if (lp != linebuf) {
857 printf(" [Incomplete last line]");
858 break;
859 }
860 return (EOF);
861 }
862 fp = genbuf;
863 }
864 if (lp >= &linebuf[LBSIZE]) {
865 error(" Line too long");
866 }
867 c = *fp++;
868 if (c == 0) {
869 cntnull++;
870 continue;
871 }
872 if (c & QUOTE) {
873 cntodd++;
874 c &= TRIM;
875 if (c == 0)
876 continue;
877 }
878 *lp++ = c;
879 } while (c != '\n');
880 cntch += lp - linebuf;
881 *--lp = 0;
882 nextip = fp;
883 cntln++;
884 return (0);
885}
886
887/*
888 * Write a range onto the io stream.
889 */
890putfile()
891{
892 line *a1;
893 register char *fp, *lp;
894 register int nib;
895
896 a1 = addr1;
897 clrstats();
898 cntln = addr2 - a1 + 1;
899 if (cntln == 0)
900 return;
901 nib = BUFSIZ;
902 fp = genbuf;
903 do {
904 getline(*a1++);
905 lp = linebuf;
906 for (;;) {
907 if (--nib < 0) {
908 nib = fp - genbuf;
909 if (write(io, genbuf, nib) != nib) {
910 wrerror();
911 }
912 cntch += nib;
913 nib = BUFSIZ - 1;
914 fp = genbuf;
915 }
916 if ((*fp++ = *lp++) == 0) {
917 fp[-1] = '\n';
918 break;
919 }
920 }
921 } while (a1 <= addr2);
922 nib = fp - genbuf;
923 if (write(io, genbuf, nib) != nib) {
924 wrerror();
925 }
926 cntch += nib;
927}
928
929/*
930 * A write error has occurred; if the file being written was
931 * the edited file then we consider it to have changed since it is
932 * now likely scrambled.
933 */
934wrerror()
935{
936
937 if (eq(file, savedfile) && edited)
938 change();
939 syserror();
940}
941
942/*
943 * Source command, handles nested sources.
944 * Traps errors since it mungs unit 0 during the source.
945 */
946static short slevel;
947
948source(fil, okfail)
949 char *fil;
950 bool okfail;
951{
952 jmp_buf osetexit;
953 register int saveinp, ointty, oerrno;
954 int oprompt;
955
956 signal(SIGINT, SIG_IGN);
957 saveinp = dup(0);
958 if (saveinp < 0)
959 error("Too many nested sources");
960 close(0);
961 if (open(fil, 0) < 0) {
962 oerrno = errno;
963 setrupt();
964 dup(saveinp);
965 close(saveinp);
966 errno = oerrno;
967 if (!okfail)
968 filioerr(fil);
969 return;
970 }
971 slevel++;
972 ointty = intty;
973 intty = isatty(0);
974 oprompt = value(PROMPT);
975 value(PROMPT) &= intty;
976 getexit(osetexit);
977 setrupt();
978 if (setexit() == 0)
979 commands(1, 1);
980 else if (slevel > 1) {
981 close(0);
982 dup(saveinp);
983 close(saveinp);
984 slevel--;
985 resexit(osetexit);
986 reset();
987 }
988 intty = ointty;
989 value(PROMPT) = oprompt;
990 close(0);
991 dup(saveinp);
992 close(saveinp);
993 slevel--;
994 resexit(osetexit);
995}
996
997/*
998 * Clear io statistics before a read or write.
999 */
1000clrstats()
1001{
1002
1003 ninbuf = 0;
1004 cntch = 0;
1005 cntln = 0;
1006 cntnull = 0;
1007 cntodd = 0;
1008}
1009
1010/*
1011 * Io is finished, close the unit and print statistics.
1012 */
1013iostats()
1014{
1015
1016 close(io);
1017 io = -1;
1018 if (hush == 0) {
1019 if (value(TERSE))
1020 printf(" %d/%D", cntln, cntch);
1021 else
1022 printf(" %d line%s, %D character%s", cntln, plural((long) cntln),
1023 cntch, plural(cntch));
1024 if (cntnull || cntodd) {
1025 printf(" (");
1026 if (cntnull) {
1027 printf("%D null", cntnull);
1028 if (cntodd)
1029 printf(", ");
1030 }
1031 if (cntodd)
1032 printf("%D non-ASCII", cntodd);
1033 putchar(')');
1034 }
1035 noonl();
1036 flush();
1037 }
1038 return (cntnull != 0 || cntodd != 0);
1039}