:1#include "V.h"
#include "memchunk.h"
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*    say [option]... word...
*
* This is program that merges BSD-and-Sys/V echo,  and  understands  both  the
* "-n"and  "\c"  conventions  for  ending lines without a newline.  Now if the
* folks that bring us Unix would see fit to do something this trivial and  put
* it in their libraries...
*
* As  an  extra  goody,  say  can either process or not process \-style escape
* sequences in the words.  This is to correct a problem with many  version  of
* the echo command, for which something like:
*    echo 'foo\r' >> bar
* results in 'foor' being written, and the backslash disappears.  With the "-"
* or  -e  or  -N  option,  say copies its params literally, without processing
* any escape sequences.
*
* The options generally start with '-' to suppress a feature or  with  '+'  to
* enable it.  For some options, either '-' or '+' may be used; for these, only
* '-' is shown, but you may use '+' if you wish.  If both are shown, then  the
* meanings are different.
*
* -    Don't process escape sequences.
*
* ?
* -?
* -h   Produce a brief help menu.
*
* The following options  control  case  mappings  by  using  'C'  to  indicate
* upper-case and 'c' to indicate lower-case. This is mostly useful if you wish
* to modify the case of words delivered by other programs.  Note the +f option
* which makes these case mappings apply to each field in a filename.
*
* -C   Upper-case first chars.
* -c   lower-case first chars.
* -cc  lower-case everything.
* -CC  Upper-case everything.
* -Cc  Upper-case first chars, lower-case the rest.
*
* -d[l][file]
* -D[l][file]
*      Set debug level to l, output to file [default: -V1].  The debug level
*      may be also set in the D_say environment variable.
*
* +e   Process escape sequences [default].
* -e   Don't process escape sequences (\c).
*
* -f   Word syntax: case options apply to each arg [default].
* +f   Filename syntax: case options apply after each '/'.
*
* The following options control case mappings by using 'U' for Upper, 'L'  for
* lower, and 'M' for mixed; either case may be used in the option:
*
* -l   lower-case first chars.
* -u   Upper-case first chars.
* -ll  lower-case everything.
* -ul  Upper-case first chars, lower-case the rest.
* -uu  Upper-case everything.
* -lm  Lower-case first chars, mixed-case the rest.
* -um  Upper-case first chars, mixed-case the rest.
* -mm  Mixed-case everything [default].
*
* -n   Don't append a newline.
* +n   Append a newline [default].
*
* -N   Don't process escape sequences or append a newline.
* +N   Process escape sequences or append a newline [default].
*
* -1   Non-unique: show args that repeat earlier ones [default].
* +1   Unique: omit args that repeat earlier ones.
* -r   Suppress repeats: omit args that repeat earlier ones.
* +r   Allow repeats: show args that repeat earlier ones [default].
*
* -R<n>
* +R<n>
*     Repeat the output <n> times.  If omitted, <n> defaults to a very
*     large number.
*
* -S<n>
* +S<n>
*     Sleep <n> seconds between repeats.  The default is 0 seconds; +S
*     without the <n> means +S1 (one second between lines of output).
*
* >f
*     Output redirection to file f.  This is useful when say is execed
*     from  other  programs.  Note that you must quote this arg (or at
*     least the '>') when dealing with a shell, else  the  shell  will
*     process it.
*
* These options should precede any words to which they are to apply.  The case
* options may be specified in the CASES environment variable; it is overridden
* by the command-line case options.
*
* An interesting use of the +1 (or -r) option is in  reducing  your  $path  to
* eliminate the replications that are easy to get if you say something like:
*    set path = ($cwd/{.,bin,sh} $path /etc)
* If you do this more than once, you can easily end up with a very long search
* path with lots of replications.  Instead, do something like:
*    set path = (`say -r $cwd/{.,bin,sh} $path /etc`)
* and each directory will occur only once, at its earliest point in the  list.
*
* There are many versions of csh that have a file-replicating bug:  If you use
* a pathname with multiple wildcards, such as "a*b/c**d/e*", and there is more
* than  one  possible match, csh's wildcard expansion may give you some of the
* expansions more than once.  You can use a command like:
*    files = ( `say +1 -N a*b/c**d/e*` )
* to eliminate these repetitions.
*
* This program uses JC's debug package, which can be  useful  when  you  first
* compile  it.  After you have it working, you might delete all the lines that
* start with a ':' and recompile. To enable output, you can use the -D option,
* or you can use the D_say environment variable. Both take a "value" that is a
* 1-digit debug level (default=1) followed  by  an  optional  filename  (which
* defaults to stderr).  Thus if you tell csh:
*    setenv D_say 5/tmp/say.out
* then  all  uses  of say will run at debug level 5, and write their output to
* the /tmp/say.out file.  If you say:
*    say -V3/log/say
* then that one call of say will run at debug level  3,  and  will  write  its
* output to the /log/say file.
*
* Warning:  With the -R option, options are only processed the first time,  so
* attempts to change options within the line will not be effective.
*/
#ifndef INT_MAX	/* Not all Unix libraries define this; we default to 32 bits */
#define INT_MAX 0x7FFFFFFF
#endif
char*ba = 0;	/* Output buffer */
char*bp = 0;	/* Current buffer position */
char*bz = 0;	/* Final buffer position + 1 */
int  bn = BUFSIZ;	/* Size of buffer */
Flag cfl = 0;	/* Case modification */
int  delay = 0;	/* How long to delay between repeats */
FILE*fp = 0;	/* Scratch file pointer, for redirection */
Flag lc1 = 0;	/* Lower-case first chars */
Flag lcc = 0;	/* Lower-case other chars */
Flag uc1 = 0;	/* Upper-case first chars */
Flag ucc = 0;	/* Upper-case other chars */
Flag fnm = 0;	/* Filename syntax */
Flag nlfl = 1;	/* Add newline to the output */
Flag opfl = 1;	/* Look for options */
Flag prfl = 1;	/* Process escape sequences */
int  times = 1;	/* Number of times to repeat the output */
Flag unfl = 0;	/* Uniqueify the output */

main(ac,av) char**av;
{	int  a, i;
	int  c, c0, c1, d;
	int  words=0;
	char*p;

:1	ac = Vinit(ac,av);

	if (p = Getenv("CASES"))
		caseopt(p);
loop:
	words = 0;
	for (a=1; a<ac; a++) {
:3		V3 "Arg%3d: \"%s\"",a,av[a] D;
		p = av[a];
		c0 = *p++;
		if (!c0) continue;
		if (opfl) {		/* Options allowed? */
			Switch(c0) {
			  case '-': case '+':
:2				V2 "Option: \"%s\"",av[a] D;
				c1 = *p++;
				Switch(c1) {
				  case '1':		/* Uniquify the output */
					unfl = (c0 == '+');
					break;
				  case  0 :
				  case 'E':	case 'e':	/* How to handle escape sequences */
					prfl = (c0 == '+');
					break;
				  case 'D': case 'd':
					Vopt(p);
					break;
				  case 'F': case 'f':	/* Filename syntax */
					fnm = (c0 == '+');
					break;
				  case 'H': case 'h':
				  case '?':
					help();
					break;
				  case 'N':		/* Newline/escape processing combined */
					nlfl = prfl = (c0 == '+');
					break;
				  case 'n':		/* Newline at end */
					nlfl = (c0 == '+');
					break;
				  case 'C': case 'c':	/* Case modification options */
				  case 'L': case 'l':
				  case '-':
				  case 'M': case 'm':
				  case 'U': case 'u':
					caseopt(av[a]+1);
					break;
				  case 'R':
					if (sscanf(p,"%d",&times) < 1) {
						times = INT_MAX;
:2						V3 "Repeat forever." D;
:2					} else {
:2						V3 "Repeat %d times.",times D;
					}
					break;
				  case 'r':		/* Suppress repeated args */
					unfl = (c0 == '-');
					break;
				  case 'S':
					if (sscanf(p,"%d",&delay) < 1)
						delay = 1;
:2					V3 "Repeat delay: %d sec.",delay D;
					break;
				  default:
:2					V2 "%s: Unknown option \"%s\" ignored.",av[0],av[a] D;
					break;
				}
				av[a][0] = 0;	/* Suppress options if repeating */
				continue;
	          case '>':			/* Kludge to do file redirection */
:2				V2 "Output: \"%s\"",av[a] D;
	            if (fp = fopen(p,"w")) {
	                dup2(fileno(fp),fileno(stdout));
	                fclose(fp);
:1	            } else {
:1	                P1 "%s: Can't write \"%s\" [Err %d=%s]",pname,p,Erreason D;
	            }
				av[a][0] = 0;	/* Suppress options if repeating */
	            continue;
			} /* Not an option; write the field */
		}
		if (cfl) {
			modcase(av[a]);	/* Case modification */
			c0 = av[a][0];
		}
		if (unfl) {			/* Unique output? */
			for (i=1; i<a; i++) {	/* Run thru earlier args */
				if (strcmp(av[i],av[a]) == 0) {
:3					V3 "Matched arg %d, dropped.",i D;
					goto done;	/* Horrify the anti-goto crowd */
				}
			}
		}
		if (words++)
			outchar(' ');		/* Blank separator between words */
		p = av[a];
:2		V4 "\"%s\"",av[a] D;
		while (c = *p++) {
			if (!prfl) {		/* Are we processing the words? */
				outchar(c);		/* If not, output literally */
			} else Switch(c) {
			  case '\\':
				Switch(c = *p++) {
				  case 'c': nlfl = 0; break;
				  case 'b': outchar('\b'); break;	/* Back space */
				  case 'f': outchar('\f'); break;	/* Form feed */
				  case 'n': outchar('\n'); break;	/* New line */
				  case 'r': outchar('\r'); break;	/* Carriage Return */
				  case 't': outchar('\t'); break;	/* Horizontal tab */
				  case 'v': outchar('\v'); break;	/* Vertical tab */
				  case '\\':outchar('\\'); break;	/* Back slash */
				  case '0': case '1': case '2':
				  case '3': case '4': case '5':		/* Octal constant */
				  case '6': case '7':
					d = c - '0';
					for (i=0; i<2; i++) {		/* Use at most 3 digits */
						if ((c = *p) && ('0' <= c) && (c <= '9')) {
							d = (d << 3) + (c - '0');
							p++;
						}
					}
				  	outchar(d);
					break;
				  default:
				 	 outchar(c);
					 break;
				  case 0: break;
				}
				break;
			  default: outchar(c); break;
			  case 0: break;
			}
		}
done:	;	/* Done with one arg */
:5		V5 "Arg%3d: \"%s\" done.",a,av[a] D;
	}
	if (nlfl) {
:2		V4 "'\\n'" D;
		outchar('\n');
	}
	if (--times > 0) {
:5		V5 "%d times left...",times D;
		if (delay > 0)
			Sleep(delay);
		Loop;
	}
	exit(0);
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Do the case modifications for one field.  Note that we do it in-place.
*/
modcase(p)
	char *p;
{	int   c;
	Flag  first=1;
	char *q=p;
:1	Fpush("modcase");
	while (c = *p) {
		while (fnm && (c == '/')) {
:5			V5 "slash: '%c'",dsp(c) D;
			first = 1;
			c = *++p;
		}
		if (first) {	/* First chars */
:5			V5 "first: '%c'",dsp(c) D;
			if (uc1) *p = toupper(c);
			if (lc1) *p = tolower(c);
		} else {		/* Other chars */
:5			V5 "other: '%c'",dsp(c) D;
			if (ucc) *p = toupper(c);
			if (lcc) *p = tolower(c);
		}
:6		V6 "Field: \"%s\"",q D;
		first = 0;
		++p;
	}
:1	Fpop;
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*/
outchar(c)
{
:1	Fpush("outchar");
	if (!ba) {
:2		V4 "Gotta allocate %d-byte buffer.",bn D;
		if (!(bp = ba = (CP)GetChunk(bn,"buffer")))
			Fail;
		bz = ba + bn;
	}
	if (bp >= bz) {
:2		P2 "%s\t\"%s\"",Vtime(),Dsps(ba,bp-ba) D;
		Write(1,ba,bp-ba);
		bp = ba;
	}
	*bp++ = c;
	Switch(c) {
	  case '\r':
	  case '\n':
:2		P2 "%s\t\"%s\"",Vtime(),Dsps(ba,bp-ba) D;
		Write(1,ba,bp-ba);
		bp = ba;
	  break;
	}
fail:
:1	Fpop;
	return c;
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Process a case option.
*/
caseopt(p)
	char *p;
{	int   c;

:1	Fpush("caseopt");
	Switch(p[0]) {	/* First chars */
	  case 'u': case 'U': case 'C': uc1 = cfl = 1; break;
	  case 'l': case 'L': case 'c': lc1 = cfl = 1; break;
	  case 'm': case 'M': case '-': uc1 = lc1 = 0; break;
	  default: Fail;
	}
	Switch(p[1]) {	/* Other chars */
	  case 'u': case 'U': case 'C': ucc = cfl = 1; break;
	  case 'l': case 'L': case 'c': lcc = cfl = 1; break;
	  case 'm': case 'M': case '-': ucc = lcc = 0; break;
	  default: Fail;
	}
	Done;
fail:
:2	V2 "Can't handle \"%s\" case option.",p D;
done:
:3	V4 "cfl=%d uc1=%d ucc=%d lc1=%d lcc=%d",cfl,uc1,ucc,lc1,lcc D;
:1	Fpop;
}
help()
{
	printf("   -\tDon't process escape sequences.\n");
	printf("   -h\tProduce this help menu.\n");
	printf("   -Cu\tUpper-case first chars, lower-case the rest.\n");
	printf("   -D<l><file>\tDebug level <l>, write to <file>\n");
	printf("   +e\tProcess escape sequences [default].\n");
	printf("   +f\tFilename syntax: case options apply after each '/'.\n");
	printf("   -[lmu][lmu]\tCase of first char and of everything else.\n");
	printf("   -n\tDon't append a newline.\n");
	printf("   +n\tAppend a newline [default].\n");
	printf("   +N\tProcess escape sequences or append a newline [default].\n");
	printf("   +1\tUnique: omit args that repeat earlier ones.\n");
	printf("   +r\tAllow repeats: show args that repeat earlier ones [default].\n");
	printf("   -R<n>\tRepeat the output <n> times. If omitted, <n> defaults to a large number.\n");
	printf("   -S<n>\tSleep <n> seconds between repeats.  Default = 0 seconds.\n");
	printf("   >f\tOutput redirection, useful when invoked from other programs.\n");
	printf("Environment:\n");
	printf("   D_%s\tDebug level and file.\n",pname);
}
