Index | Thread | Search

From:
joshua stein <jcs@jcs.org>
Subject:
Re: /bin/echo: add -e to handle escape sequences
To:
tech@openbsd.org
Date:
Wed, 25 Feb 2026 09:00:50 -0600

Download raw body.

Thread
anyone?


On Sat, 21 Feb 2026 at 18:54:52 -0600, joshua stein wrote:
> diff --git bin/echo/echo.1 bin/echo/echo.1
> index 7286602d752..cfd0391eb44 100644
> --- bin/echo/echo.1
> +++ bin/echo/echo.1
> @@ -41,7 +41,7 @@
>  .Nd write arguments to the standard output
>  .Sh SYNOPSIS
>  .Nm echo
> -.Op Fl n
> +.Op Fl Een
>  .Op Ar string ...
>  .Sh DESCRIPTION
>  The
> @@ -63,6 +63,41 @@ is treated as part of
>  .Pp
>  The options are as follows:
>  .Bl -tag -width Ds
> +.It Fl E
> +Disable interpretation of backslash escape sequences (default).
> +.It Fl e
> +Enable interpretation of the following backslash escape sequences:
> +.Pp
> +.Bl -tag -width Ds -offset indent -compact
> +.It Cm \e\e
> +A literal backslash.
> +.It Cm \ea
> +Alert (BEL).
> +.It Cm \eb
> +Backspace.
> +.It Cm \ec
> +Suppress further output, including the trailing newline character.
> +.It Cm \ee
> +Escape character.
> +.It Cm \ef
> +Form feed.
> +.It Cm \en
> +Newline.
> +.It Cm \er
> +Carriage return.
> +.It Cm \et
> +Horizontal tab.
> +.It Cm \ev
> +Vertical tab.
> +.It Cm \e0 Ns Ar nnn
> +The character whose octal value is
> +.Ar nnn
> +(zero to three octal digits).
> +.It Cm \ex Ns Ar hh
> +The character whose hexadecimal value is
> +.Ar hh
> +(one or two hexadecimal digits).
> +.El
>  .It Fl n
>  Do not print the trailing newline character.
>  .El
> @@ -79,17 +114,17 @@ utility is compliant with the
>  .St -p1003.1-2008
>  specification.
>  .Pp
> -The flag
> +The flags
> +.Op Fl E ,
> +.Op Fl e ,
> +and
>  .Op Fl n
> -conflicts with the behaviour mandated by the
> +conflict with the behaviour mandated by the
>  X/Open System Interfaces option of the
>  .St -p1003.1-2008
>  specification,
> -which says it should be treated as part of
> +which says they should be treated as part of
>  .Ar string .
> -Additionally,
> -.Nm
> -does not support any of the backslash character sequences mandated by XSI.
>  .Pp
>  .Nm
>  also exists as a built-in to
> diff --git bin/echo/echo.c bin/echo/echo.c
> index 52da05c050f..ba0b368d990 100644
> --- bin/echo/echo.c
> +++ bin/echo/echo.c
> @@ -30,29 +30,51 @@
>   * SUCH DAMAGE.
>   */
>  
> +#include <ctype.h>
>  #include <stdio.h>
>  #include <string.h>
>  #include <unistd.h>
>  #include <err.h>
>  
> +int escape(const char *);
> +
>  int
>  main(int argc, char *argv[])
>  {
> -	int nflag;
> +	int nflag = 0, eflag = 0;
> +	const char *p;
>  
>  	if (pledge("stdio", NULL) == -1)
>  		err(1, "pledge");
>  
>  	/* This utility may NOT do getopt(3) option parsing. */
> -	if (*++argv && !strcmp(*argv, "-n")) {
> -		++argv;
> -		nflag = 1;
> +	for (++argv; argv[0] && *argv[0] == '-'; argv++) {
> +		for (p = *argv + 1; *p != '\0'; p++) {
> +			switch (*p) {
> +			case 'E':
> +				eflag = 0;
> +				break;
> +			case 'e':
> +				eflag = 1;
> +				break;
> +			case 'n':
> +				nflag = 1;
> +				break;
> +			default:
> +				eflag = nflag = 0;
> +				goto echoargs;
> +			}
> +		}
>  	}
> -	else
> -		nflag = 0;
>  
> +echoargs:
>  	while (*argv) {
> -		(void)fputs(*argv, stdout);
> +		if (eflag) {
> +			if (escape(*argv) != 0)
> +				/* \c encountered */
> +				return 0;
> +		} else
> +			(void)fputs(*argv, stdout);
>  		if (*++argv)
>  			putchar(' ');
>  	}
> @@ -61,3 +83,86 @@ main(int argc, char *argv[])
>  
>  	return 0;
>  }
> +
> +/* return -1 on \c to suppress further output */
> +int
> +escape(const char *s)
> +{
> +	int ch, n;
> +
> +	while ((ch = *s++) != '\0') {
> +		if (ch != '\\') {
> +			putchar(ch);
> +			continue;
> +		}
> +
> +		switch ((ch = *s++)) {
> +		case '\0':
> +			putchar('\\');
> +			return 0;
> +		case '\\':
> +			putchar('\\');
> +			break;
> +		case 'a':
> +			putchar('\a');
> +			break;
> +		case 'b':
> +			putchar('\b');
> +			break;
> +		case 'c':
> +			return -1;
> +		case 'e':
> +			putchar('\033');
> +			break;
> +		case 'f':
> +			putchar('\f');
> +			break;
> +		case 'n':
> +			putchar('\n');
> +			break;
> +		case 'r':
> +			putchar('\r');
> +			break;
> +		case 't':
> +			putchar('\t');
> +			break;
> +		case 'v':
> +			putchar('\v');
> +			break;
> +		case '0':
> +			/* octal: \0nnn */
> +			ch = 0;
> +			for (n = 0; n < 3 && *s >= '0' && *s <= '7'; n++)
> +				ch = ch * 8 + (*s++ - '0');
> +			putchar(ch);
> +			break;
> +		case 'x':
> +			/* hexadecimal: \xhh */
> +			if (isxdigit((unsigned char)*s)) {
> +				ch = 0;
> +				for (n = 0;
> +				    n < 2 && isxdigit(*s); n++) {
> +					ch *= 16;
> +					if (*s >= '0' && *s <= '9')
> +						ch += *s - '0';
> +					else if (*s >= 'a' && *s <= 'f')
> +						ch += *s - 'a' + 10;
> +					else
> +						ch += *s - 'A' + 10;
> +					s++;
> +				}
> +				putchar(ch);
> +			} else {
> +				putchar('\\');
> +				putchar('x');
> +			}
> +			break;
> +		default:
> +			putchar('\\');
> +			putchar(ch);
> +			break;
> +		}
> +	}
> +
> +	return 0;
> +}
>