[comp.sources.unix] v17i091: A printf program

rsalz@uunet.uu.net (Rich Salz) (02/09/89)

Submitted-by: Chris Torek <chris@mimsy.umd.edu>
Posting-number: Volume 17, Issue 91
Archive-name: printf

Printf duplicates (as far as possible) the standard C library routine of
the same name, at the shell command level.  It is similar to echo, except
that it formats its arguments according to conversion specifications given
in the format-string before writing them to the standard output.  For a
thorough explanation of format specifications, see printf(3s).

It implements most of the ANSI specification, as well as Roman Numerals.
	-Chris

: Run this shell script with "sh" not "csh"
PATH=/bin:/usr/bin:/usr/ucb:/etc:$PATH
export PATH
all=false
if [ x$1 = x-a ]; then
	all=true
fi
echo Extracting printf.1
sed 's/^X//' <<'//go.sysin dd *' >printf.1
X.\"	@(#)printf.1	8-Jan-1987
X.\"
X.TH PRINTF 1 "8-Jan-1987"
X.UC 4
X.SH NAME
Xprintf \- formatted output at shell command level
X.SH SYNOPSIS
X.B printf 
X.I format-string
X[
X.I arg1
X] [
X.I arg2
X] ...
X.SH DESCRIPTION
X.I Printf
Xduplicates (as far as possible) the standard C library routine of the
Xsame name, at the shell command level.  It is similar to
X.IR echo ,
Xexcept that it formats its arguments according to conversion specifications
Xgiven in the
X.I format-string
Xbefore writing them to the standard output.
XFor a thorough explanation of format specifications, see
X.IR printf (3s).
X.PP
XFor the sake of perversity,
X.I printf
Ximplements one format conversion
X.B not
Xsupported by the standard printf subroutine: the
X.I %r
Xand
X.IR %R
Xconversions, which print integers as Roman numerals.  The
Xfirst format produces lowercase, and the second uppercase.
X.PP
XAs a convenience, within the
X.I format-string
Xand any string or character arguments,
X.I printf
Xconverts ``backslash notation'' \- as defined in the
Xdraft proposed ANSI C Standard X3J11 \- into the
Xappropriate control characters.
XThe Standard provides for hexadecimal escapes as ``\ex1a2F3c4...'', in
Xwhich the only way to terminate the escape is with a non-hexadecimal
Xcharacter.  This is not always suitable outside the C language, so
X.I printf
Xprovides one additional escape,
X.BR \e& ,
Xwhich expands to nothing, but in so doing serves to terminate a
Xhexadecimal escape.
X.SH EXAMPLES
X.nf
X.na
X.ta 0.6i
X.sp 2
X% printf 'Today is %s the %d of %s.\en' Monday 1 April
XToday is Monday the 1 of April.
X.sp 3
X% printf 'Interesting Numbers\en\en\etPie: %*.*f\en\etFoo: %g\en' \e
X	6 4 3.14159265 42
XInteresting Numbers
X
X	Pie: 3.1416
X	Foo: 42
X.sp 3
X% printf '%s %d, %R\en' July 4 1776
XJuly 4, MDCCLXXVI
X.sp 3
X% printf 'in ASCII this prints dd: \ex64\e&d.\en' 
Xin ASCII this prints dd: dd.
X.sp 2
X.fi
X.ad
X.SH AUTHORS
XFred Blonder <fred@mimsy.umd.edu>
X.sp
XChris Torek <chris@mimsy.umd.edu>
X.SH "SEE ALSO"
Xecho(1), printf(3s)
X.SH BUGS
XThe Roman conversions are not strictly correct.
XZero produces no text;
Xvery large values give the complaint ``abortive Roman numeral''.
XNegative Roman numerals are printed with a leading minus sign.
XIt is unclear what the Romans did in such cases,
Xalthough zero could perhaps be written as ``nihil''.
XValues in the millions were sometimes written
Xusing an M with an overbar,
Xbut there is no bar-M character in ASCII.
X.sp
XThe ``%n'' conversion is unimplementable.
XThe number of characters written is not returned.
XLong double formats are not supported.
//go.sysin dd *
if [ `wc -c < printf.1` != 2521 ]; then
	made=false
	echo error transmitting printf.1 --
	echo length should be 2521, not `wc -c < printf.1`
else
	made=true
fi
if $made; then
	chmod 444 printf.1
	echo -n '	'; ls -ld printf.1
fi
echo Extracting printf.c
sed 's/^X//' <<'//go.sysin dd *' >printf.c
X#ifndef lint
Xstatic char rcsid[]= "$Header: printf.c,v 2.3 88/08/19 03:41:12 chris Exp $";
X#endif
X
X/*
X * printf - duplicate the C library routine of the same name, but from
X * the shell command level.
X *
X * This version by Chris Torek, based on an earlier version by Fred Blonder.
X */
X
X#include <stdio.h>
X#include <ctype.h>
X#include <sysexits.h>
X
Xchar	*progname;
X
Xchar	*ctor(), **doit();
Xdouble	atof();
Xint	atoi();
Xlong	atol();
X
Xmain(argc, argv)
X	int argc;
X	char **argv;
X{
X	register char *cp, *convp, **ap, **ep;
X	register int ch, ndyn, flags;
X	char cbuf[BUFSIZ];	/* separates each conversion */
X	static char hasmod[] = "has integer length modifier";
X
X	/* flags */
X#define	LONG	1
X#define	SHORT	2
X
X	ap = argv;
X	ep = &ap[argc];
X	progname = *ap++;
X	if (argc < 2) {
X		(void) fprintf(stderr,
X			"%s: Usage: %s <format-string> [ arg1 . . . ]\n",
X			progname, progname);
X		exit(EX_USAGE);
X	}
X
X	ctrl(cp = *ap++);	/* backslash interpretation of fmt string */
X
X	/*
X	 * Scan format string for conversion specifications.
X	 * (The labels would be loops, but then everything falls
X	 * off the right.)
X	 */
Xscan:
X	while ((ch = *cp++) != '%') {
X		if (ch == 0)
X			exit(EX_OK);
X		(void) putchar(ch);
X	}
X
X	ndyn = 0;
X	flags = 0;
X	convp = cbuf;
X	*convp++ = ch;
X
X	/* scan for conversion character */
Xcvt:
X	switch (ch = *cp++) {
X
X	case '\0':	/* unterminated conversion */
X		exit(EX_OK);
X
X	/* string or character format */
X	case 'c': case 's':
X		if (flags)
X			illfmt(cbuf, convp, ch, hasmod);
X		ap = doit(cbuf, convp, ap, ep, ndyn, ch, ch);
X		goto scan;
X
X	/* integer formats */
X	case 'd': case 'i': case 'o': case 'u': case 'x': case 'X':
X		if ((flags & (LONG|SHORT)) == (LONG|SHORT))
X			illfmt(cbuf, convp, ch, "is both long and short");
X		ap = doit(cbuf, convp, ap, ep, ndyn, ch,
X			flags & LONG ? 'l' : flags & SHORT ? 'h' : 'i');
X		goto scan;
X
X	/* floating point formats */
X	case 'e': case 'E': case 'f': case 'g': case 'G':
X		if (flags)
X			illfmt(cbuf, convp, ch, hasmod);
X		ap = doit(cbuf, convp, ap, ep, ndyn, ch, 'f');
X		goto scan;
X
X	/* Roman (well, why not?) */
X	case 'r': case 'R':
X		if (flags)
X			illfmt(cbuf, convp, ch, hasmod);
X		ap = doit(cbuf, convp, ap, ep, ndyn, 's', ch);
X		goto scan;
X
X	case '%':	/* boring */
X		(void) putchar('%');
X		goto scan;
X
X	/* short integers */
X	case 'h':
X		flags |= SHORT;
X		break;
X
X	/* long integers */
X	case 'l':
X		flags |= LONG;
X		break;
X
X	/* field-width or precision specifier, or flag: keep scanning */
X	case '.': case '#': case '-': case '+': case ' ':
X	case '0': case '1': case '2': case '3': case '4':
X	case '5': case '6': case '7': case '8': case '9':
X		break;
X
X	/* dynamic field width or precision: count it */
X	case '*':
X		ndyn++;
X		break;
X
X	default:	/* something we cannot handle */
X		if (isascii(ch) && isprint(ch))
X			cbuf[0] = ch, cbuf[1] = 0;
X		else
X			(void) sprintf(cbuf, "\\%03o", (unsigned char)ch);
X		(void) fprintf(stderr,
X			"%s: illegal conversion character `%s'\n",
X			progname, cbuf);
X		exit(EX_USAGE);
X		/* NOTREACHED */
X	}
X
X	/* 2 leaves room for ultimate conversion char and for \0 */
X	if (convp >= &cbuf[sizeof(cbuf) - 2]) {
X		(void) fprintf(stderr, "%s: conversion string too long\n",
X			progname);
X		exit(EX_USAGE);
X	}
X	*convp++ = ch;
X	goto cvt;
X}
X
Xillfmt(cbuf, convp, ch, why)
X	char *cbuf, *convp;
X	int ch;
X	char *why;
X{
X
X	*convp++ = ch;
X	*convp = 0;
X	(void) fprintf(stderr, "%s: format `%s' illegal: %s\n",
X		progname, cbuf, why);
X	exit(EX_USAGE);
X}
X
X/*
X * Emit a conversion.  cch holds the printf format character for
X * this conversion; cty holds a simplified version (all integer
X * conversions, e.g., are represented as 'i').
X */
Xchar **
Xdoit(cbuf, convp, ap, ep, ndyn, cch, cty)
X	char *cbuf, *convp;
X	register char **ap;
X	char **ep;
X	register int ndyn;
X	int cch, cty;
X{
X	register char *s;
X	union {		/* four basic conversion types */
X		int i;
X		long l;
X		double d;
X		char *str;
X	} arg;
X	int a1, a2;	/* dynamic width and/or precision */
X
X	/* finish off the conversion string */
X	s = convp;
X	*s++ = cch;
X	*s = 0;
X	s = cbuf;
X
X	/* verify number of arguments */
X	if (&ap[ndyn] >= ep) {
X		(void) fprintf(stderr,
X			"%s: not enough args for format `%s'\n",
X			progname, s);
X		exit(EX_USAGE);
X	}
X
X	/* pick up dynamic specifiers */
X	if (ndyn) {
X		a1 = atoi(*ap++);
X		if (ndyn > 1)
X			a2 = atoi(*ap++);
X		if (ndyn > 2) {
X			(void) fprintf(stderr,
X				"%s: too many `*'s in `%s'\n",
X				progname, s);
X			exit(EX_USAGE);
X		}
X	}
X
X#define	PRINTF(what) \
X	if (ndyn == 0) \
X		(void) printf(s, what); \
X	else if (ndyn == 1) \
X		(void) printf(s, a1, what); \
X	else \
X		(void) printf(s, a1, a2, what);
X
X	/* emit the appropriate conversion */
X	switch (cty) {
X
X	/* string */
X	case 's':
X		ctrl(arg.str = *ap++);
X		goto string;
X
X	/* roman (much like string) */
X	case 'r': case 'R':
X		arg.str = ctor(atoi(*ap++), cty == 'R');
Xstring:
X		PRINTF(arg.str);
X		break;
X
X	/* floating point */
X	case 'f':
X		arg.d = atof(*ap++);
X		PRINTF(arg.d);
X		break;
X
X	/* character */
X	case 'c':
X		ctrl(*ap);
X		arg.i = *(*ap++);
X		goto integer;
X
X	/* short integer */
X	case 'h':
X		arg.i = (short) atoi(*ap++);
X		goto integer;
X
X	/* integer */
X	case 'i':
X		arg.i = atoi(*ap++);
Xinteger:
X		PRINTF(arg.i);
X		break;
X
X	/* long integer */
X	case 'l':
X		arg.l = atol(*ap++);
X		PRINTF(arg.l);
X		break;
X	}
X	return (ap);
X}
X
X/*
X * Return the index of the character c in the string s; character 0
X * is NOT considered part of the string (unlike index() or strchr()).
X * If c is not found (or is 0), return -1.
X *
X * This is used for hex and octal digit conversions in ctrl().
X */
Xint
Xdigit(s, c)
X	char *s;
X	register int c;
X{
X	register char *p;
X
X	for (p = s; *p; p++)
X		if (*p == c)
X			return (p - s);
X	return (-1);
X}
X
X/*
X * Convert backslash notation to control characters, in place.
X */
Xctrl(s)
X	register char *s;
X{
X	register char *op = s;
X	register int v, c;
X	static char oct[] = "01234567";
X	static char hex[] = "0123456789abcdefABCDEF";
X
X	while ((c = *s++) != 0) {
X		if (c != '\\') {
X			*op++ = c;
X			continue;
X		}
X		switch (*s++) {
X		case '\0':	/* end-of-string: user goofed */
X			s--;
X			break;
X
X		case '\\':	/* backslash */
X			*op++ = '\\';
X			break;
X
X		case 'n':	/* newline */
X			*op++ = '\n';
X			break;
X
X		case 't':	/* horizontal tab */
X			*op++ = '\t';
X			break;
X
X		case 'r':	/* carriage-return */
X			*op++ = '\r';
X			break;
X
X		case 'f':	/* form-feed */
X			*op++ = '\f';
X			break;
X
X		case 'b':	/* backspace */
X			*op++ = '\b';
X			break;
X
X		case 'v':	/* vertical tab */
X			*op++ = '\13';
X			break;
X
X		case 'a':	/* WARNING! DANGER! DANGER! DANGER! */
X			*op++ = '\7';
X			break;
X
X		case '0': case '1': case '2': case '3':
X		case '4': case '5': case '6': case '7':
X			/* octal constant, 3 digits maximum */
X			v = digit(oct, s[-1]);
X			if ((c = digit(oct, *s)) >= 0) {
X				v = (v << 3) + c;
X				if ((c = digit(oct, *++s)) >= 0) {
X					v = (v << 3) + c;
X					s++;
X				}
X			}
X			*op++ = v;
X			break;
X
X		case 'x':	/* hex constant */
X			v = 0;
X			while ((c = digit(hex, *s)) >= 0) {
X				if (c >= 16)
X					c -= 6;
X				v = (v << 4) + c;
X				s++;
X			}
X			*op++ = v;
X			break;
X
X		/*
X		 * The name of this object is taken from troff:
X		 * \z might be better, but this has a precedent.
X		 * It exists solely so that we can end a hex constant
X		 * which must be followed by a legal hex character.
X		 */
X		case '&':	/* special zero-width `character' */
X			break;
X
X		default:
X			*op++ = s[-1];
X		}
X	}
X	*op = '\0';
X}
X
X/*
X * Convert integer to Roman Numerals. (How have you survived without it?)
X */
Xchar *
Xctor(x, caps)
X	int x, caps;
X{
X	static char buf[BUFSIZ];
X	register char *outp = buf;
X	register unsigned n = x;
X	register int u, v;
X	register char *p, *q;
X
X	if ((int)n < 0) {
X		*outp++ = '-';
X		n = -n;
X	}
X	p = caps ? "M\2D\5C\2L\5X\2V\5I" : "m\2d\5c\2l\5x\2v\5i";
X	v = 1000;
X	if (n >= v * BUFSIZ / 2)	/* conservative */
X		return ("[abortive Roman numeral]");
X	for (;;) {
X		while (n >= v)
X			*outp++ = *p, n -= v;
X		if (n == 0)
X			break;
X		q = p + 1;
X		u = v / *q;
X		if (*q == 2)		/* magic */
X			u /= *(q += 2);
X		if (n + u >= v) {
X			*outp++ = *++q;
X			n += u;
X		} else {
X			p++;
X			v /= *p++;
X		}
X	}
X	*outp = 0;
X	return (buf);
X}
//go.sysin dd *
if [ `wc -c < printf.c` != 8021 ]; then
	made=false
	echo error transmitting printf.c --
	echo length should be 8021, not `wc -c < printf.c`
else
	made=true
fi
if $made; then
	chmod 444 printf.c
	echo -n '	'; ls -ld printf.c
fi
-- 
Please send comp.sources.unix-related mail to rsalz@uunet.uu.net.