Skip site navigation (1)Skip section navigation (2)
Date:      Sun, 8 Sep 1996 17:56:40 -0700 (PDT)
From:      Mark Diekhans <markd@Grizzly.COM>
To:        freebsd-hackers@freebsd.org
Subject:   First pass at XPG/3 style positional arguments for *printf functions
Message-ID:  <199609090056.RAA00220@osprey.grizzly.com>

next in thread | raw e-mail | index | archive | help

    Enclosed is a patch to 2.2-960801-SNAP lib/libc/stdio/vfprintf.c and 
lib/libc/stdio/printf.3 to implement the XPG/3 style positional arguments
(e.g. %2$s) to the *printf family of functions.  Its been tested against
a program derived from the Tcl tests, so I believe its fairly solid.

This functionality is essential for building localized software.  It is part
most current libc implementations (including GNU).  Comments welcome and if
any one wants to work with me on getting this into current, that would be
great.

Mark


*** printf.3.ORG	Sun Sep  8 13:56:14 1996
--- printf.3	Sun Sep  8 15:15:18 1996
***************
*** 176,181 ****
--- 176,191 ----
  the following appear in sequence:
  .Bl -bullet
  .It
+ An optional field, consisting of a decimal digit string followed by a
+ .Cm $ ,
+ specifying the next argument to access .
+ If this field is not provided, the argument following the last
+ argument accessed will be used.
+ Arguments are numbered starting at
+ .Cm 1 .
+ If unaccessed arguments in the format string are interspersed with ones that
+ are accessed the results will be indeterminate.
+ .It
  Zero or more of the following flags:
  .Bl -hyphen
  .It
***************
*** 394,399 ****
--- 404,411 ----
  A field width or precision, or both, may be indicated by
  an asterisk
  .Ql *
+ or an asterisk followed by one or more decimal digits and a
+ .Ql $
  instead of a
  digit string.
  In this case, an
***************
*** 402,407 ****
--- 414,421 ----
  A negative field width is treated as a left adjustment flag followed by a
  positive field width; a negative precision is treated as though it were
  missing.
+ If a single format directive mixes positional (nn$)
+ and non-positional arguments, the results are undefined.
  .Pp
  The conversion specifiers and their meanings are:
  .Bl -tag -width "diouxX"
*** vfprintf.c.ORG	Sat Sep  7 19:47:59 1996
--- vfprintf.c	Sun Sep  8 16:16:35 1996
***************
*** 75,80 ****
--- 75,82 ----
  static int	__sbprintf __P((FILE *, const char *, va_list));
  static char *	__ultoa __P((u_long, char *, int, int, char *));
  static char *	__uqtoa __P((u_quad_t, char *, int, int, char *));
+ static void	__find_arguments __P((const char *, va_list, void ***));
+ static void	__grow_type_table __P((int, unsigned char **, int *));
  
  /*
   * Flush out all the vectors defined by the given uio,
***************
*** 274,279 ****
--- 276,282 ----
  
  #endif /* FLOATING_POINT */
  
+ #define STATIC_ARG_TBL_SIZE 8           /* Size of static argument table. */
  
  /*
   * Flags used during conversion.
***************
*** 295,301 ****
  {
  	register char *fmt;	/* format string */
  	register int ch;	/* character from fmt */
! 	register int n;		/* handy integer (short term usage) */
  	register char *cp;	/* handy char pointer (short term usage) */
  	register struct __siov *iovp;/* for PRINT macro */
  	register int flags;	/* flags as above */
--- 298,304 ----
  {
  	register char *fmt;	/* format string */
  	register int ch;	/* character from fmt */
! 	register int n, n2;	/* handy integer (short term usage) */
  	register char *cp;	/* handy char pointer (short term usage) */
  	register struct __siov *iovp;/* for PRINT macro */
  	register int flags;	/* flags as above */
***************
*** 323,328 ****
--- 326,335 ----
  	struct __siov iov[NIOV];/* ... and individual io vectors */
  	char buf[BUF];		/* space for %c, %[diouxX], %[eEfgG] */
  	char ox[2];		/* space for 0x hex-prefix */
+         void **argtable;        /* args, built due to positional arg */
+         void *statargtable [STATIC_ARG_TBL_SIZE];
+         int nextarg;            /* 1-based argument index */
+         va_list orgap;          /* original argument pointer */
  
  	/*
  	 * Choose PADSIZE to trade efficiency vs. size.  If larger printf
***************
*** 365,382 ****
  	iovp = iov; \
  }
  
  	/*
  	 * To extend shorts properly, we need both signed and unsigned
  	 * argument extraction methods.
  	 */
  #define	SARG() \
! 	(flags&LONGINT ? va_arg(ap, long) : \
! 	    flags&SHORTINT ? (long)(short)va_arg(ap, int) : \
! 	    (long)va_arg(ap, int))
  #define	UARG() \
! 	(flags&LONGINT ? va_arg(ap, u_long) : \
! 	    flags&SHORTINT ? (u_long)(u_short)va_arg(ap, int) : \
! 	    (u_long)va_arg(ap, u_int))
  
  #ifdef _THREAD_SAFE
  	_thread_flockfile(fp,__FILE__,__LINE__);
--- 372,424 ----
  	iovp = iov; \
  }
  
+         /*
+          * Get the argument indexed by nextarg.   If the argument table is
+          * built, use it to get the argument.  If its not, get the next
+          * argument (and arguments must be gotten sequentially).
+          */
+ #define GETARG(type) \
+         ((argtable != NULL) ? *((type*)(argtable[nextarg++])) : \
+             (nextarg++, va_arg(ap, type)))
+ 
  	/*
  	 * To extend shorts properly, we need both signed and unsigned
  	 * argument extraction methods.
  	 */
  #define	SARG() \
! 	(flags&LONGINT ? GETARG(long) : \
! 	    flags&SHORTINT ? (long)(short)GETARG(int) : \
! 	    (long)GETARG(int))
  #define	UARG() \
! 	(flags&LONGINT ? GETARG(u_long) : \
! 	    flags&SHORTINT ? (u_long)(u_short)GETARG(int) : \
! 	    (u_long)GETARG(u_int))
! 
!         /*
!          * Get * arguments, including the form *nn$.  Preserve the nextarg
!          * that the argument can be gotten once the type is determined.
!          */
! #define GETASTER(val) \
!         n2 = 0; \
!         cp = fmt; \
!         while (is_digit(*cp)) { \
!                 n2 = 10 * n2 + to_digit(*cp); \
!                 cp++; \
!         } \
!         if (*cp == '$') { \
!             	int hold = nextarg; \
!                 if (argtable == NULL) { \
!                         argtable = statargtable; \
!                         __find_arguments (fmt0, orgap, &argtable); \
!                 } \
!                 nextarg = n2; \
!                 val = GETARG (int); \
!                 nextarg = hold; \
!                 fmt = ++cp; \
!         } else { \
! 		val = GETARG (int); \
!         }
!         
  
  #ifdef _THREAD_SAFE
  	_thread_flockfile(fp,__FILE__,__LINE__);
***************
*** 399,404 ****
--- 441,449 ----
  	}
  
  	fmt = (char *)fmt0;
+         argtable = NULL;
+         nextarg = 1;
+         orgap = ap;
  	uio.uio_iov = iovp = iov;
  	uio.uio_resid = 0;
  	uio.uio_iovcnt = 0;
***************
*** 445,451 ****
  			 *	-- ANSI X3J11
  			 * They don't exclude field widths read from args.
  			 */
! 			if ((width = va_arg(ap, int)) >= 0)
  				goto rflag;
  			width = -width;
  			/* FALLTHROUGH */
--- 490,497 ----
  			 *	-- ANSI X3J11
  			 * They don't exclude field widths read from args.
  			 */
! 			GETASTER (width);
! 			if (width >= 0)
  				goto rflag;
  			width = -width;
  			/* FALLTHROUGH */
***************
*** 457,463 ****
  			goto rflag;
  		case '.':
  			if ((ch = *fmt++) == '*') {
! 				n = va_arg(ap, int);
  				prec = n < 0 ? -1 : n;
  				goto rflag;
  			}
--- 503,509 ----
  			goto rflag;
  		case '.':
  			if ((ch = *fmt++) == '*') {
! 				GETASTER (n);
  				prec = n < 0 ? -1 : n;
  				goto rflag;
  			}
***************
*** 483,488 ****
--- 529,543 ----
  				n = 10 * n + to_digit(ch);
  				ch = *fmt++;
  			} while (is_digit(ch));
+ 			if (ch == '$') {
+ 				nextarg = n;
+                         	if (argtable == NULL) {
+                                 	argtable = statargtable;
+                                 	__find_arguments (fmt0, orgap,
+ 						&argtable);
+ 				}
+ 				goto rflag;
+                         }
  			width = n;
  			goto reswitch;
  #ifdef FLOATING_POINT
***************
*** 500,506 ****
  			flags |= QUADINT;
  			goto rflag;
  		case 'c':
! 			*(cp = buf) = va_arg(ap, int);
  			size = 1;
  			sign = '\0';
  			break;
--- 555,561 ----
  			flags |= QUADINT;
  			goto rflag;
  		case 'c':
! 			*(cp = buf) = GETARG(int);
  			size = 1;
  			sign = '\0';
  			break;
***************
*** 510,516 ****
  		case 'd':
  		case 'i':
  			if (flags & QUADINT) {
! 				uqval = va_arg(ap, quad_t);
  				if ((quad_t)uqval < 0) {
  					uqval = -uqval;
  					sign = '-';
--- 565,571 ----
  		case 'd':
  		case 'i':
  			if (flags & QUADINT) {
! 				uqval = GETARG(quad_t);
  				if ((quad_t)uqval < 0) {
  					uqval = -uqval;
  					sign = '-';
***************
*** 536,544 ****
  fp_begin:		if (prec == -1)
  				prec = DEFPREC;
  			if (flags & LONGDBL)
! 				_double = (double)va_arg(ap, long double);
  			else
! 				_double = va_arg(ap, double);
  			/* do this before tricky precision changes */
  			if (isinf(_double)) {
  				if (_double < 0)
--- 591,599 ----
  fp_begin:		if (prec == -1)
  				prec = DEFPREC;
  			if (flags & LONGDBL)
! 				_double = (double)GETARG(long double);
  			else
! 				_double = GETARG(double);
  			/* do this before tricky precision changes */
  			if (isinf(_double)) {
  				if (_double < 0)
***************
*** 588,607 ****
  #endif /* FLOATING_POINT */
  		case 'n':
  			if (flags & QUADINT)
! 				*va_arg(ap, quad_t *) = ret;
  			else if (flags & LONGINT)
! 				*va_arg(ap, long *) = ret;
  			else if (flags & SHORTINT)
! 				*va_arg(ap, short *) = ret;
  			else
! 				*va_arg(ap, int *) = ret;
  			continue;	/* no output */
  		case 'O':
  			flags |= LONGINT;
  			/*FALLTHROUGH*/
  		case 'o':
  			if (flags & QUADINT)
! 				uqval = va_arg(ap, u_quad_t);
  			else
  				ulval = UARG();
  			base = 8;
--- 643,662 ----
  #endif /* FLOATING_POINT */
  		case 'n':
  			if (flags & QUADINT)
! 				*GETARG(quad_t *) = ret;
  			else if (flags & LONGINT)
! 				*GETARG(long *) = ret;
  			else if (flags & SHORTINT)
! 				*GETARG(short *) = ret;
  			else
! 				*GETARG(int *) = ret;
  			continue;	/* no output */
  		case 'O':
  			flags |= LONGINT;
  			/*FALLTHROUGH*/
  		case 'o':
  			if (flags & QUADINT)
! 				uqval = GETARG(u_quad_t);
  			else
  				ulval = UARG();
  			base = 8;
***************
*** 614,627 ****
  			 * defined manner.''
  			 *	-- ANSI X3J11
  			 */
! 			ulval = (u_long)va_arg(ap, void *);
  			base = 16;
  			xdigs = "0123456789abcdef";
  			flags = (flags & ~QUADINT) | HEXPREFIX;
  			ch = 'x';
  			goto nosign;
  		case 's':
! 			if ((cp = va_arg(ap, char *)) == NULL)
  				cp = "(null)";
  			if (prec >= 0) {
  				/*
--- 669,682 ----
  			 * defined manner.''
  			 *	-- ANSI X3J11
  			 */
! 			ulval = (u_long)GETARG(void *);
  			base = 16;
  			xdigs = "0123456789abcdef";
  			flags = (flags & ~QUADINT) | HEXPREFIX;
  			ch = 'x';
  			goto nosign;
  		case 's':
! 			if ((cp = GETARG(char *)) == NULL)
  				cp = "(null)";
  			if (prec >= 0) {
  				/*
***************
*** 646,652 ****
  			/*FALLTHROUGH*/
  		case 'u':
  			if (flags & QUADINT)
! 				uqval = va_arg(ap, u_quad_t);
  			else
  				ulval = UARG();
  			base = 10;
--- 701,707 ----
  			/*FALLTHROUGH*/
  		case 'u':
  			if (flags & QUADINT)
! 				uqval = GETARG(u_quad_t);
  			else
  				ulval = UARG();
  			base = 10;
***************
*** 657,663 ****
  		case 'x':
  			xdigs = "0123456789abcdef";
  hex:			if (flags & QUADINT)
! 				uqval = va_arg(ap, u_quad_t);
  			else
  				ulval = UARG();
  			base = 16;
--- 712,718 ----
  		case 'x':
  			xdigs = "0123456789abcdef";
  hex:			if (flags & QUADINT)
! 				uqval = GETARG(u_quad_t);
  			else
  				ulval = UARG();
  			base = 16;
***************
*** 809,817 ****
--- 864,1195 ----
  #ifdef _THREAD_SAFE
  	_thread_funlockfile(fp);
  #endif
+         if ((argtable != NULL) && (argtable != statargtable))
+                 free (argtable);
  	return (ret);
  	/* NOTREACHED */
  }
+ 
+ /*
+  * Type ids for argument type table.
+  */
+ #define T_UNUSED	0
+ #define T_SHORT		1
+ #define T_U_SHORT	2
+ #define TP_SHORT	3
+ #define T_INT		4
+ #define T_U_INT		5
+ #define TP_INT		6
+ #define T_LONG		7
+ #define T_U_LONG	8
+ #define TP_LONG		9
+ #define T_QUAD		10
+ #define T_U_QUAD	11
+ #define TP_QUAD		12
+ #define T_DOUBLE	13
+ #define T_LONG_DOUBLE	14
+ #define TP_CHAR		15
+ #define TP_VOID		16
+ 
+ /*
+  * Find all arguments when a positional parameter is encountered.  Returns a
+  * table, indexed by argument number, of pointers to each arguments.  The
+  * initial argument table should be an array of STATIC_ARG_TBL_SIZE entries.
+  * It will be replaces with a malloc-ed on if it overflows.
+  */ 
+ static void
+ __find_arguments (fmt0, ap, argtable)
+ 	const char *fmt0;
+ 	va_list ap;
+ 	void ***argtable;
+ {
+ 	register char *fmt;	/* format string */
+ 	register int ch;	/* character from fmt */
+ 	register int n, n2;	/* handy integer (short term usage) */
+ 	register char *cp;	/* handy char pointer (short term usage) */
+ 	register int flags;	/* flags as above */
+ 	int width;		/* width from format (%8d), or 0 */
+ 	unsigned char *typetable; /* table of types */
+ 	unsigned char stattypetable [STATIC_ARG_TBL_SIZE];
+ 	int tablesize;		/* current size of type table */
+ 	int tablemax;		/* largest used index in table */
+ 	int nextarg;		/* 1-based argument index */
+ 
+ 	/*
+ 	 * Add an argument type to the table, expanding if necessary.
+ 	 */
+ #define ADDTYPE(type) \
+ 	((nextarg >= tablesize) ? \
+ 		__grow_type_table(nextarg, &typetable, &tablesize) : 0, \
+ 	typetable[nextarg++] = type, \
+ 	(nextarg > tablemax) ? tablemax = nextarg : 0)
+ 
+ #define	ADDSARG() \
+ 	((flags&LONGINT) ? ADDTYPE(T_LONG) : \
+ 		((flags&SHORTINT) ? ADDTYPE(T_SHORT) : ADDTYPE(T_INT)))
+ 
+ #define	ADDUARG() \
+ 	((flags&LONGINT) ? ADDTYPE(T_U_LONG) : \
+ 		((flags&SHORTINT) ? ADDTYPE(T_U_SHORT) : ADDTYPE(T_U_INT)))
+ 
+ 	/*
+ 	 * Add * arguments to the type array.
+ 	 */
+ #define ADDASTER() \
+ 	n2 = 0; \
+ 	cp = fmt; \
+ 	while (is_digit(*cp)) { \
+ 		n2 = 10 * n2 + to_digit(*cp); \
+ 		cp++; \
+ 	} \
+ 	if (*cp == '$') { \
+ 		int hold = nextarg; \
+ 		nextarg = n2; \
+ 		ADDTYPE (T_INT); \
+ 		nextarg = hold; \
+ 		fmt = ++cp; \
+ 	} else { \
+ 		ADDTYPE (T_INT); \
+ 	}
+ 	fmt = (char *)fmt0;
+ 	typetable = stattypetable;
+ 	tablesize = STATIC_ARG_TBL_SIZE;
+ 	tablemax = 0; 
+ 	nextarg = 1;
+ 	memset (typetable, T_UNUSED, STATIC_ARG_TBL_SIZE);
+ 
+ 	/*
+ 	 * Scan the format for conversions (`%' character).
+ 	 */
+ 	for (;;) {
+ 		for (cp = fmt; (ch = *fmt) != '\0' && ch != '%'; fmt++)
+ 			/* void */;
+ 		if (ch == '\0')
+ 			goto done;
+ 		fmt++;		/* skip over '%' */
+ 
+ 		flags = 0;
+ 		width = 0;
+ 
+ rflag:		ch = *fmt++;
+ reswitch:	switch (ch) {
+ 		case ' ':
+ 		case '#':
+ 			goto rflag;
+ 		case '*':
+ 			ADDASTER ();
+ 			goto rflag;
+ 		case '-':
+ 		case '+':
+ 			goto rflag;
+ 		case '.':
+ 			if ((ch = *fmt++) == '*') {
+ 				ADDASTER ();
+ 				goto rflag;
+ 			}
+ 			while (is_digit(ch)) {
+ 				ch = *fmt++;
+ 			}
+ 			goto reswitch;
+ 		case '0':
+ 			goto rflag;
+ 		case '1': case '2': case '3': case '4':
+ 		case '5': case '6': case '7': case '8': case '9':
+ 			n = 0;
+ 			do {
+ 				n = 10 * n + to_digit(ch);
+ 				ch = *fmt++;
+ 			} while (is_digit(ch));
+ 			if (ch == '$') {
+ 				nextarg = n;
+ 				goto rflag;
+ 			}
+ 			width = n;
+ 			goto reswitch;
+ #ifdef FLOATING_POINT
+ 		case 'L':
+ 			flags |= LONGDBL;
+ 			goto rflag;
+ #endif
+ 		case 'h':
+ 			flags |= SHORTINT;
+ 			goto rflag;
+ 		case 'l':
+ 			flags |= LONGINT;
+ 			goto rflag;
+ 		case 'q':
+ 			flags |= QUADINT;
+ 			goto rflag;
+ 		case 'c':
+ 			ADDTYPE(T_INT);
+ 			break;
+ 		case 'D':
+ 			flags |= LONGINT;
+ 			/*FALLTHROUGH*/
+ 		case 'd':
+ 		case 'i':
+ 			if (flags & QUADINT) {
+ 				ADDTYPE(T_QUAD);
+ 			} else {
+ 				ADDSARG();
+ 			}
+ 			break;
+ #ifdef FLOATING_POINT
+ 		case 'e':
+ 		case 'E':
+ 		case 'f':
+ 		case 'g':
+ 		case 'G':
+ 			if (flags & LONGDBL)
+ 				ADDTYPE(T_LONG_DOUBLE);
+ 			else
+ 				ADDTYPE(T_DOUBLE);
+ 			break;
+ #endif /* FLOATING_POINT */
+ 		case 'n':
+ 			if (flags & QUADINT)
+ 				ADDTYPE(TP_QUAD);
+ 			else if (flags & LONGINT)
+ 				ADDTYPE(TP_LONG);
+ 			else if (flags & SHORTINT)
+ 				ADDTYPE(TP_SHORT);
+ 			else
+ 				ADDTYPE(TP_INT);
+ 			continue;	/* no output */
+ 		case 'O':
+ 			flags |= LONGINT;
+ 			/*FALLTHROUGH*/
+ 		case 'o':
+ 			if (flags & QUADINT)
+ 				ADDTYPE(T_U_QUAD);
+ 			else
+ 				ADDUARG();
+ 			break;
+ 		case 'p':
+ 			ADDTYPE(TP_VOID);
+ 			break;
+ 		case 's':
+ 			ADDTYPE(TP_CHAR);
+ 			break;
+ 		case 'U':
+ 			flags |= LONGINT;
+ 			/*FALLTHROUGH*/
+ 		case 'u':
+ 			if (flags & QUADINT)
+ 				ADDTYPE(T_U_QUAD);
+ 			else
+ 				ADDUARG();
+ 			break;
+ 		case 'X':
+ 		case 'x':
+ 			if (flags & QUADINT)
+ 				ADDTYPE(T_U_QUAD);
+ 			else
+ 				ADDUARG();
+ 			break;
+ 		default:	/* "%?" prints ?, unless ? is NUL */
+ 			if (ch == '\0')
+ 				goto done;
+ 			break;
+ 		}
+ 	}
+ done:
+ 	/*
+ 	 * Build the argument table.
+ 	 */
+ 	if (tablemax >= STATIC_ARG_TBL_SIZE) {
+ 		*argtable = (void **)
+ 		    malloc (sizeof (void *) * (tablemax + 1));
+ 	}
+ 
+ 	(*argtable) [0] = NULL;
+ 	for (n = 1; n <= tablemax; n++) {
+ 		(*argtable) [n] = ap;
+ 		switch (typetable [n]) {
+ 		    case T_UNUSED:
+ 			(void) va_arg (ap, int);
+ 			break;
+ 		    case T_SHORT:
+ 			(void) va_arg (ap, int);
+ 			break;
+ 		    case T_U_SHORT:
+ 			(void) va_arg (ap, int);
+ 			break;
+ 		    case TP_SHORT:
+ 			(void) va_arg (ap, short *);
+ 			break;
+ 		    case T_INT:
+ 			(void) va_arg (ap, int);
+ 			break;
+ 		    case T_U_INT:
+ 			(void) va_arg (ap, unsigned int);
+ 			break;
+ 		    case TP_INT:
+ 			(void) va_arg (ap, int *);
+ 			break;
+ 		    case T_LONG:
+ 			(void) va_arg (ap, long);
+ 			break;
+ 		    case T_U_LONG:
+ 			(void) va_arg (ap, unsigned long);
+ 			break;
+ 		    case TP_LONG:
+ 			(void) va_arg (ap, long *);
+ 			break;
+ 		    case T_QUAD:
+ 			(void) va_arg (ap, quad_t);
+ 			break;
+ 		    case T_U_QUAD:
+ 			(void) va_arg (ap, u_quad_t);
+ 			break;
+ 		    case TP_QUAD:
+ 			(void) va_arg (ap, quad_t *);
+ 			break;
+ 		    case T_DOUBLE:
+ 			(void) va_arg (ap, double);
+ 			break;
+ 		    case T_LONG_DOUBLE:
+ 			(void) va_arg (ap, long double);
+ 			break;
+ 		    case TP_CHAR:
+ 			(void) va_arg (ap, char *);
+ 			break;
+ 		    case TP_VOID:
+ 			(void) va_arg (ap, void *);
+ 			break;
+ 		}
+ 	}
+ 
+ 	if ((typetable != NULL) && (typetable != stattypetable))
+ 		free (typetable);
+ }
+ 
+ /*
+  * Increase the size of the type table.
+  */
+ static void
+ __grow_type_table (nextarg, typetable, tablesize)
+ 	int nextarg;
+ 	unsigned char **typetable;
+ 	int *tablesize;
+ {
+ 	unsigned char *oldtable = *typetable;
+ 	int newsize = *tablesize * 2;
+ 
+ 	if (*tablesize == STATIC_ARG_TBL_SIZE) {
+ 		*typetable = (unsigned char *)
+ 		    malloc (sizeof (unsigned char) * newsize);
+ 		bcopy (oldtable, *typetable, *tablesize);
+ 	} else {
+ 		*typetable = (unsigned char *)
+ 		    realloc (typetable, sizeof (unsigned char) * newsize);
+ 
+ 	}
+ 	memset (&typetable [*tablesize], T_UNUSED, (newsize - *tablesize));
+ 
+ 	*tablesize = newsize;
+ }
+ 
  
  #ifdef FLOATING_POINT
  





Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?199609090056.RAA00220>