Index | Thread | Search

From:
joshua stein <jcs@jcs.org>
Subject:
/bin/echo: add -e to handle escape sequences
To:
tech@openbsd.org
Date:
Sat, 21 Feb 2026 14:38:50 -0600

Download raw body.

Thread
(This comes out of a thread on ports@ about cups-filters)

This adds support to /bin/echo for -e to enable processing escape 
sequences like printf(1) and ksh's internal echo do (and -E to 
disable it).

ksh's internal echo processes an unlimited number of arguments 
starting with a dash:

    $ echo -n -n "test"
    test

but our current /bin/echo only supports one:

    $ /bin/oldecho -n -n "test"
    -n test

At Theo's request, this implementation keeps that handling and only 
processes one argument starting with a dash:

    $ ./obj/echo -ne "hi\thi"
    hi	hi

    $ ./obj/echo -n -e "hi\thi"
    -e hi\thi

Any unknown flags in that first set make it print the whole thing as 
echo currently does:

    $ ./obj/echo -neJ "hi\thi"
    -neJ hi\thi

Theo would like more input on this; should our ksh internal echo act 
the same way regarding multiple - arguments, or should echo behave 
like ksh?

FWIW, Linux's coreutils behaves like our ksh.


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..7f4919ff84b 100644
--- bin/echo/echo.c
+++ bin/echo/echo.c
@@ -30,29 +30,53 @@
  * 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;
+	if (*++argv && *argv[0] == '-') {
+		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;
+			}
+		}
+
+		argv++;
 	}
-	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 +85,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;
+}