From: joshua stein Subject: /bin/echo: add -e to handle escape sequences To: tech@openbsd.org Date: Sat, 21 Feb 2026 14:38:50 -0600 (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 #include #include #include #include +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; +}