17

I was writing a Makefile (on Ubuntu 20.04, if it's relevant) and noticed some interesting behavior with echo. Take this simple Makefile:

test.txt:
        @echo -e 'hello\nworld'
        @echo -e 'hello\nworld' > test.txt

When I run make, I would expect to see the same thing on stdout as in test.txt, but in fact I do not. I get this on stdout:

hello
world

but this in test.txt:

-e hello
world

Meanwhile, if I remove -e from both lines in the Makefile, I get this on stdout:

hello\nworld

and this in test.txt:

hello
world

This had me wondering if echo detects the redirection and behaves differently, but it doesn't when I just run it manually in the shell with /bin/echo -e 'hello\nworld' > test.txt (which yields hello and world on separate lines, as I would normally expect). I even went so far as to confirm that the Makefile is using /bin/echo instead of a shell builtin by adding an @echo --version line.

What is going on here?

1 Answers1

28

UNIX compliant implementations of echo are required to output -e<space>hello<newline>world<newline> there.

Those that don't are not compliant. Many aren't which means it's almost impossible to use echo portably, printf should be used instead. bash's echo, in some (most) builds of it, is only compliant when you enable both the posix and xpg_echo options. That might be the echo behaviour you were expecting.

Same for the echo standalone utility that comes with GNU coreutils which is only compliant if it's invoked with $POSIXLY_CORRECT set in its environment (and is of a recent enough version).

make normally runs sh to interpret the command lines on each action line.

However, the GNU implementation of make, as an optimisation, can run commands directly if the code is simple enough and it thinks it doesn't need to invoke a shell to interpret it.

That explains why echo --version gives you /bin/echo, but echo ... > file needs a shell to perform the redirection.

You can use strace -fe execve make to see what make executes (or the truss/tusc... equivalent on your system if not Linux).

Here, it seems that while your /bin/echo is not compliant, your sh has a echo builtin that is compliant.

Here, use printf if you want to expand echo-style escape sequences:

printf '%b\n' 'hello\nworld'

In its format argument, printf understands C-style escape sequences (there's a difference with the echo-style ones for the \0xxx (echo) vs \xxx (C) octal sequences)

printf 'hello\nworld\n'

Here, you could also do:

printf '%s\n' hello world

Which is the common and portable way to output several arguments on separate lines.

Another approach would be to add:

SHELL = bash

To your Makefile for make to invoke bash (assuming it's installed and found in $PATH) instead of sh to interpret the command lines. Or invoke make as make <target> SHELL=bash.

That won't necessarily make it more portable as while there are more and more systems where bash is installed by default these days, there are some (like on Solaris) where bash is built so that its echo builtin behaves the standard way by default.

Setting SHELL to anything other than /bin/sh also has the side effect of disabling GNU make's optimisation mentioned above, so with make <target> SHELL=sh or make <target> SHELL=/bin//sh, you'd get consistent behaviours between the two invocation, while still not having to add a dependency to bash.

  • I assume you meant "POSIX-compliant" not "UNIX-compliant" in the first sentence – Esther Apr 29 '22 at 16:31
  • @Esther, POSIX merged with the Single UNIX Specification (formerly X/Open Portability Guide) in SUSv3/POSIX2001, with the difference between the two identified with options (mostly XSI: X/Open System Interface). Here, what I call UNIX is POSIX+XSI. With POSIX alone, the behaviour of that echo -e '...\...' is unspecified because of the backslash. – Stéphane Chazelas May 04 '22 at 07:15