text: #include <u.h>
  #include <9pm/libc.h>
  #include <9pm/bio.h>
  #include <9pm/ctype.h>
  
  /*
   *	PR command (print files in pages and columns, with headings)
   *	2+head+2+page[56]+5
   */
  
  #define	ISPRINT(c)	((c) >= ' ')
  #define ESC		'\033'
  #define LENGTH		66
  #define LINEW		72
  #define NUMW		5
  #define MARGIN		10
  #define DEFTAB		8
  #define NFILES		10
  #define HEAD		"%12.12s %4.4s  %s Page %d\n\n\n", date+4, date+24, head, Page
  #define TOLOWER(c)	(isupper(c) ? tolower(c) : c)	/* ouch! */
  #define cerror(S)	fprint(2, "pr: %s", S)
  #define STDINNAME()	nulls
  #define TTY		"/dev/cons", 0
  #define PROMPT()	fprint(2, "\a") /* BEL */
  #define TABS(N,C)	if((N = intopt(argv, &C)) < 0) N = DEFTAB
  #define ETABS		(Inpos % Etabn)
  #define ITABS		(Itabn > 0 && Nspace >= (nc = Itabn - Outpos % Itabn))
  #define NSEPC		'\t'
  #define EMPTY		14	/* length of " -- empty file" */
  
  typedef	struct	Fils	Fils;
  typedef	struct	Colp*	Colp;
  typedef	struct	Err	Err;
  
  struct	Fils
  {
  	Biobuf*	f_f;
  	char*	f_name;
  	long	f_nextc;
  };
  struct	Colp
  {
  	Rune*	c_ptr;
  	Rune*	c_ptr0;
  	long	c_lno;
  };
  struct	Err
  {
  	Err*	e_nextp;
  	char*	e_mess;
  };
  
  int	Balance = 0;
  Biobuf	bout;
  Rune*	Bufend;
  Rune*	Buffer = 0;
  int	C = '\0';
  Colp	Colpts;
  int	Colw;
  int	Dblspace = 1;
  Err*	err = 0;
  int	error = 0;
  int	Etabc = '\t';
  int	Etabn = 0;
  Fils*	Files;
  int	Formfeed = 0;
  int	Fpage = 1;
  char*	Head = 0;
  int	Inpos;
  int	Itabc = '\t';
  int	Itabn = 0;
  Err*	Lasterr = (Err*)&err;
  int	Lcolpos;
  int	Len = LENGTH;
  int	Line;
  int	Linew = 0;
  long	Lnumb = 0;
  int	Margin = MARGIN;
  int	Multi = 0;
  int	Ncols = 1;
  int	Nfiles = 0;
  int	Nsepc = NSEPC;
  int	Nspace;
  char	nulls[] = "";
  int	Numw;
  int	Offset = 0;
  int	Outpos;
  int	Page;
  int	Pcolpos;
  int	Plength;
  int	Sepc = 0;
  
  extern	int	atoix(char**);
  extern	void	balance(int);
  extern	void	die(char*);
  extern	void	errprint(void);
  extern	char*	ffiler(char*);
  extern	int	findopt(int, char**);
  extern	int	get(int);
  extern	void*	getspace(ulong);
  extern	int	intopt(char**, int*);
  extern	void	main(int, char**);
  extern	Biobuf*	mustopen(char*, Fils*);
  extern	void	nexbuf(void);
  extern	int	pr(char*);
  extern	void	put(long);
  extern	void	putpage(void);
  extern	void	putspace(void);
  
  /*
   * return date file was last modified
   */
  char*
  getdate(void)
  {
  	static char *now = 0;
  	static Dir sbuf, nbuf;
  
  	if(Nfiles > 1 || Files->f_name == nulls) {
  		if(now == 0) {
  			nbuf.mtime = time(0);
  			now = ctime(nbuf.mtime);
  		}
  		return now;
  	}
  	dirstat(Files->f_name, &sbuf);
  	return ctime(sbuf.mtime);
  }
  
  char*
  ffiler(char *s)
  {
  	static char buf[100];
  
  	sprint(buf, "can't open %s\n", s);
  	return buf;
  }
  
  void
  main(int argc, char *argv[])
  {
  	Fils fstr[NFILES];
  	int nfdone = 0;
  
  	Binit(&bout, 1, OWRITE);
  	Files = fstr;
  	for(argc = findopt(argc, argv); argc > 0; --argc, ++argv)
  		if(Multi == 'm') {
  			if(Nfiles >= NFILES - 1)
  				die("too many files");
  			if(mustopen(*argv, &Files[Nfiles++]) == 0)
  				nfdone++; /* suppress printing */
  		} else {
  			if(pr(*argv))
  				Bterm(Files->f_f);
  			nfdone++;
  		}
  	if(!nfdone)			/* no files named, use stdin */
  		pr(nulls);		/* on GCOS, use current file, if any */
  	errprint();			/* print accumulated error reports */
  	if(error)
  		exits("error");
  	exits(0);
  }
  
  int
  findopt(int argc, char *argv[])
  {
  	char **eargv = argv;
  	int eargc = 0, c;
  
  	while(--argc > 0) {
  		switch(c = **++argv) {
  		case '-':
  			if((c = *++*argv) == '\0')
  				break;
  		case '+':
  			do {
  				if(isdigit(c)) {
  					--*argv;
  					Ncols = atoix(argv);
  				} else
  				switch(c = TOLOWER(c)) {
  				case '+':
  					if((Fpage = atoix(argv)) < 1)
  						Fpage = 1;
  					continue;
  				case 'd':
  					Dblspace = 2;
  					continue;
  				case 'e':
  					TABS(Etabn, Etabc);
  					continue;
  				case 'f':
  					Formfeed++;
  					continue;
  				case 'h':
  					if(--argc > 0)
  						Head = argv[1];
  					continue;
  				case 'i':
  					TABS(Itabn, Itabc);
  					continue;
  				case 'l':
  					Len = atoix(argv);
  					continue;
  				case 'a':
  				case 'm':
  					Multi = c;
  					continue;
  				case 'o':
  					Offset = atoix(argv);
  					continue;
  				case 's':
  					if((Sepc = (*argv)[1]) != '\0')
  						++*argv;
  					else
  						Sepc = '\t';
  					continue;
  				case 't':
  					Margin = 0;
  					continue;
  				case 'w':
  					Linew = atoix(argv);
  					continue;
  				case 'n':
  					Lnumb++;
  					if((Numw = intopt(argv, &Nsepc)) <= 0)
  						Numw = NUMW;
  				case 'b':
  					Balance = 1;
  					continue;
  				default:
  					die("bad option");
  				}
  			} while((c = *++*argv) != '\0');
  			if(Head == argv[1])
  				argv++;
  			continue;
  		}
  		*eargv++ = *argv;
  		eargc++;
  	}
  	if(Len == 0)
  		Len = LENGTH;
  	if(Len <= Margin)
  		Margin = 0;
  	Plength = Len - Margin/2;
  	if(Multi == 'm')
  		Ncols = eargc;
  	switch(Ncols) {
  	case 0:
  		Ncols = 1;
  	case 1:
  		break;
  	default:
  		if(Etabn == 0)		/* respect explicit tab specification */
  			Etabn = DEFTAB;
  	}
  	if(Linew == 0)
  		Linew = Ncols != 1 && Sepc == 0? LINEW: 512;
  	if(Lnumb)
  		Linew -= Multi == 'm'? Numw: Numw*Ncols;
  	if((Colw = (Linew - Ncols + 1)/Ncols) < 1)
  		die("width too small");
  	if(Ncols != 1 && Multi == 0) {
  		ulong buflen = ((ulong)(Plength/Dblspace + 1))*(Linew+1)*sizeof(char);
  		Buffer = getspace(buflen*sizeof(*Buffer));
  		Bufend = &Buffer[buflen];
  		Colpts = getspace((Ncols+1)*sizeof(*Colpts));
  	}
  	return eargc;
  }
  
  int
  intopt(char *argv[], int *optp)
  {
  	int c;
  
  	if((c = (*argv)[1]) != '\0' && !isdigit(c)) {
  		*optp = c;
  		(*argv)++;
  	}
  	c = atoix(argv);
  	return c != 0? c: -1;
  }
  
  int
  pr(char *name)
  {
  	char *date = 0, *head = 0;
  
  	if(Multi != 'm' && mustopen(name, &Files[0]) == 0)
  		return 0;
  	if(Buffer)
  		Bungetc(Files->f_f);
  	if(Lnumb)
  		Lnumb = 1;
  	for(Page = 0;; putpage()) {
  		if(C == -1)
  			break;
  		if(Buffer)
  			nexbuf();
  		Inpos = 0;
  		if(get(0) == -1)
  			break;
  		Bflush(&bout);
  		Page++;
  		if(Page >= Fpage) {
  			if(Margin == 0)
  				continue;
  			if(date == 0)
  				date = getdate();
  			if(head == 0)
  				head = Head != 0 ? Head :
  					Nfiles < 2? Files->f_name: nulls;
  			Bprint(&bout, "\n\n");
  			Nspace = Offset;
  			putspace();
  			Bprint(&bout, HEAD);
  		}
  	}
  	C = '\0';
  	return 1;
  }
  
  void
  putpage(void)
  {
  	int colno;
  
  	for(Line = Margin/2;; get(0)) {
  		for(Nspace = Offset, colno = 0, Outpos = 0; C != '\f';) {
  			if(Lnumb && C != -1 && (colno == 0 || Multi == 'a')) {
  				if(Page >= Fpage) {
  					putspace();
  					Bprint(&bout, "%*ld", Numw, Buffer?
  						Colpts[colno].c_lno++: Lnumb);
  					Outpos += Numw;
  					put(Nsepc);
  				}
  				Lnumb++;
  			}
  			for(Lcolpos=0, Pcolpos=0; C!='\n' && C!='\f' && C!=-1; get(colno))
  					put(C);
  			if(C==-1 || ++colno==Ncols || C=='\n' && get(colno)==-1)
  				break;
  			if(Sepc)
  				put(Sepc);
  			else
  				if((Nspace += Colw - Lcolpos + 1) < 1)
  					Nspace = 1;
  		}
  	/*
  		if(C == -1) {
  			if(Margin != 0)
  				break;
  			if(colno != 0)
  				put('\n');
  			return;
  		}
  	*/
  		if(C == -1 && colno == 0) {
  			if(Margin != 0)
  				break;
  			return;
  		}
  		if(C == '\f')
  			break;
  		put('\n');
  		if(Dblspace == 2 && Line < Plength)
  			put('\n');
  		if(Line >= Plength)
  			break;
  	}
  	if(Formfeed)
  		put('\f');
  	else
  		while(Line < Len)
  			put('\n');
  }
  
  void
  nexbuf(void)
  {
  	Rune *s = Buffer;
  	Colp p = Colpts;
  	int j, c, bline = 0;
  
  	for(;;) {
  		p->c_ptr0 = p->c_ptr = s;
  		if(p == &Colpts[Ncols])
  			return;
  		(p++)->c_lno = Lnumb + bline;
  		for(j = (Len - Margin)/Dblspace; --j >= 0; bline++)
  			for(Inpos = 0;;) {
  				if((c = Bgetrune(Files->f_f)) == -1) {
  					for(*s = -1; p <= &Colpts[Ncols]; p++)
  						p->c_ptr0 = p->c_ptr = s;
  					if(Balance)
  						balance(bline);
  					return;
  				}
  				if(ISPRINT(c))
  					Inpos++;
  				if(Inpos <= Colw || c == '\n') {
  					*s = c;
  					if(++s >= Bufend)
  						die("page-buffer overflow");
  				}
  				if(c == '\n')
  					break;
  				switch(c) {
  				case '\b':
  					if(Inpos == 0)
  						s--;
  				case ESC:
  					if(Inpos > 0)
  						Inpos--;
  				}
  			}
  	}
  }
  
  /*
   * line balancing for last page
   */
  void
  balance(int bline)
  {
  	Rune *s = Buffer;
  	Colp p = Colpts;
  	int colno = 0, j, c, l;
  
  	c = bline % Ncols;
  	l = (bline + Ncols - 1)/Ncols;
  	bline = 0;
  	do {
  		for(j = 0; j < l; ++j)
  			while(*s++ != '\n')
  				;
  		(++p)->c_lno = Lnumb + (bline += l);
  		p->c_ptr0 = p->c_ptr = s;
  		if(++colno == c)
  			l--;
  	} while(colno < Ncols - 1);
  }
  
  int
  get(int colno)
  {
  	static int peekc = 0;
  	Colp p;
  	Fils *q;
  	long c;
  
  	if(peekc) {
  		peekc = 0;
  		c = Etabc;
  	} else
  	if(Buffer) {
  		p = &Colpts[colno];
  		if(p->c_ptr >= (p+1)->c_ptr0)
  			c = -1;
  		else
  			if((c = *p->c_ptr) != -1)
  				p->c_ptr++;
  	} else
  	if((c = (q = &Files[Multi == 'a'? 0: colno])->f_nextc) == -1) {
  		for(q = &Files[Nfiles]; --q >= Files && q->f_nextc == -1;)
  			;
  		if(q >= Files)
  			c = '\n';
  	} else
  		q->f_nextc = Bgetrune(q->f_f);
  	if(Etabn != 0 && c == Etabc) {
  		Inpos++;
  		peekc = ETABS;
  		c = ' ';
  	} else
  	if(ISPRINT(c))
  		Inpos++;
  	else
  		switch(c) {
  		case '\b':
  		case ESC:
  			if(Inpos > 0)
  				Inpos--;
  			break;
  		case '\f':
  			if(Ncols == 1)
  				break;
  			c = '\n';
  		case '\n':
  		case '\r':
  			Inpos = 0;
  		}
  	return C = c;
  }
  
  void
  put(long c)
  {
  	int move;
  
  	switch(c) {
  	case ' ':
  		Nspace++;
  		Lcolpos++;
  		return;
  	case '\b':
  		if(Lcolpos == 0)
  			return;
  		if(Nspace > 0) {
  			Nspace--;
  			Lcolpos--;
  			return;
  		}
  		if(Lcolpos > Pcolpos) {
  			Lcolpos--;
  			return;
  		}
  	case ESC:
  		move = -1;
  		break;
  	case '\n':
  		Line++;
  	case '\r':
  	case '\f':
  		Pcolpos = 0;
  		Lcolpos = 0;
  		Nspace = 0;
  		Outpos = 0;
  	default:
  		move = (ISPRINT(c) != 0);
  	}
  	if(Page < Fpage)
  		return;
  	if(Lcolpos > 0 || move > 0)
  		Lcolpos += move;
  	if(Lcolpos <= Colw) {
  		putspace();
  		Bputrune(&bout, c);
  		Pcolpos = Lcolpos;
  		Outpos += move;
  	}
  }
  
  void
  putspace(void)
  {
  	int nc;
  
  	for(; Nspace > 0; Outpos += nc, Nspace -= nc)
  		if(ITABS)
  			Bputc(&bout, Itabc);
  		else {
  			nc = 1;
  			Bputc(&bout, ' ');
  		}
  }
  
  int
  atoix(char **p)
  {
  	int n = 0, c;
  
  	while(isdigit(c = *++*p))
  		n = 10*n + c - '0';
  	(*p)--;
  	return n;
  }
  
  /*
   * Defer message about failure to open file to prevent messing up
   * alignment of page with tear perforations or form markers.
   * Treat empty file as special case and report as diagnostic.
   */
  Biobuf*
  mustopen(char *s, Fils *f)
  {
  	if(*s == '\0') {
  		f->f_name = STDINNAME();
  		f->f_f = malloc(sizeof(Biobuf));
  		if(f->f_f == 0)
  			cerror("no memory");
  		Binit(f->f_f, 0, OREAD);
  	} else
  	if((f->f_f = Bopen(f->f_name = s, OREAD)) == 0) {
  		s = ffiler(f->f_name);
  		s = strcpy((char*)getspace(strlen(s) + 1), s);
  	}
  	if(f->f_f != 0) {
  		if((f->f_nextc = Bgetrune(f->f_f)) >= 0 || Multi == 'm')
  			return f->f_f;
  		sprint(s = (char*)getspace(strlen(f->f_name) + 1 + EMPTY),
  			"%s -- empty file\n", f->f_name);
  		Bterm(f->f_f);
  	}
  	error = 1;
  	cerror(s);
  	fprint(2, "\n");
  	return 0;
  }
  
  void*
  getspace(ulong n)
  {
  	void *t;
  
  	if((t = malloc(n)) == 0)
  		die("out of space");
  	return t;
  }
  
  void
  die(char *s)
  {
  	error++;
  	errprint();
  	cerror(s);
  	Bputc(&bout, '\n');
  	exits("error");
  }
  
  /*
  void
  onintr(void)
  {
  	error++;
  	errprint();
  	exits("error");
  }
  /**/
  
  /*
   * print accumulated error reports
   */
  void
  errprint(void)
  {
  	Bflush(&bout);
  	for(; err != 0; err = err->e_nextp) {
  		cerror(err->e_mess);
  		fprint(2, "\n");
  	}
  }
  