Echo: the command, the (several) builtins, and makefiles
As explained in this post I wanted to patch a symbol in a binary file,
and for that needed to echo a specific byte value (xEA) into a
sed command appearing in a Makefile recipe.
Now, you may be aware that the echo command lives in /bin/, but
the shell also provide an echo shell builtin. Let's see how we can
echo a byte with hex value EA in both cases:
$ man echo
echo [SHORT-OPTION]... [STRING]...
echo LONG-OPTION
DESCRIPTION
Echo the STRING(s) to standard output.
-n do not output the trailing newline
-e enable interpretation of backslash escapes
[...]
If -e is in effect, the following sequences are recognized:
[...]
\xHH byte with hexadecimal value HH (1 to 2 digits)
NOTE: your shell may have its own version of echo, which usually super-
sedes the version described here. Please refer to your shell's docu-
mentation for details about the options it supports.
So we need to use the -e option to enable escape sequences, and then
use the \xEA sequence. Let's see the builtin.
$ man bash
[...]
SHELL BUILTIN COMMANDS
[...]
echo [-neE] [arg ...]
Output the args, separated by spaces, followed by a newline.
The return status is 0 unless a write error occurs. If -n is
specified, the trailing newline is suppressed. If the -e option
is given, interpretation of the following backslash-escaped
characters is enabled. The -E option disables the interpreta-
[...]
\xHH the eight-bit character whose value is the hexadecimal
value HH (one or two hex digits)
The bash echo builtin supports the same option and escape syntax, so
it seems it's irrelevant wether we run the external command or the
bash builtin.
Let's try it:
$ echo -e "\xEA" �
Obviously the character can only be displayed correctly in a terminal
supporting Latin-1 encoding. We start one with xterm -en iso8859-1
and will perform all following operations there.
echo -e "\xEA" ê echo -e \\xEA ê
This is the shell builtin running here, but as expected the results
are the same when using /bin/echo.1
Good, let's try it in a Makefile:
echo1:
echo -e "\xEA"
$ make echo1 echo -e "\xEA" -e \xEA
Hey, quite unexpected! Seems echo here does not understand -e as
an option! First, let's confirm that this is the shell builtin that's
running:
type:
type echo
$ make type type echo echo is a shell builtin
Ok. Let's try the standalone command instead.
echo2:
/bin/echo -e "\xEA"
$ make echo2 /bin/echo -e "\xEA" ê
So the builtin works ok in the terminal, but not from the Makefile?
Does make do something special to the shell that runs its commands?
Let a recipe be "run the echo command inside a bash shell".
echo3:
bash -c "echo -e \"\xEA\""
$ make echo3 bash -c "echo -e \"\xEA\"" ê
Hum… Now that I think of it, there's no reason why make would
necessarily use bash, and the doc actually says:
How to update is specified by a RECIPE. This is one or more lines to be executed by the shell (normally 'sh'), but with some extra features (*note Writing Recipes in Rules: Recipes.).
So the recipes are not run with bash, but with sh ("normally")!
On my devuan system, sh is actually dash:
ls -l /bin/sh lrwxrwxrwx 1 root root 4 Jan 5 2023 /bin/sh -> dash
So the echo run by the recipe echo1 is actually dash's echo
builtin. Let's look at dash's manual page:
echo [-n] args...
Print the arguments on the standard output, separated by spaces.
Unless the -n option is present, a newline is output following the
arguments.
If any of the following sequences of characters is encountered
during output, the sequence is not output. Instead, the specified
action is performed:
\b A backspace character is output.
\c Subsequent output is suppressed. This is normally used at
the end of the last argument to suppress the trailing new-
line that echo would otherwise output.
\e Outputs an escape character (ESC).
\f Output a form feed.
\n Output a newline character.
\r Output a carriage return.
\t Output a (horizontal) tab character.
\v Output a vertical tab.
\0digits
Output the character whose value is given by zero to three
octal digits. If there are zero digits, a nul character
is output.
\\ Output a backslash.
All other backslash sequences elicit undefined behaviour.
Saw that? This builtin does not know about the -e option; escape
sequences are always honored, except that the \xHH is unknown!
However the \0OOO one is supported. Let's make sure:
$ sh -c "echo \"\0352\"" ê
Okay. By the way, if we don't like the escaped quotes \" we can
escape the \ instead. So, while this obviously does not work
$ sh -c "echo \0352" 0352
this should (the first \ protects the second one so that echo
actually receives \0352 as argument):
sh -c "echo \\0352" 0352
hoooo, except it doesn't!? Oh, well let's try this
$ sh -c "echo \\\0352" $ ê
Do you get it? bash processes the echo \\\0352 string, so \\ is
turned into \ (and \0 is unchanged2), so dash receives echo
\\0352 as a command line, parses it as the command echo and the
argument \0352. Let's try this directly in dash:
$ dash $ echo \\0352 ê
So we should be able to put this in the Makefile:
echo4:
echo \\\0352
$ make echo4 echo \\\0352 \0352
Oh my fxxx god, what's that? I must have missed something. Let's keep calm and proceed step by step:
echo5:
sh -c "echo \"\0352\""
$ make echo5 sh -c "echo \"\0352\"" ê
Good. Let's see how make runs the shell:
5.3.2 Choosing the Shell The program used as the shell is taken from the variable 'SHELL'. If this variable is not set in your makefile, the program '/bin/sh' is used as the shell. The argument(s) passed to the shell are taken from the variable '.SHELLFLAGS'. The default value of '.SHELLFLAGS' is '-c' normally, or '-ec' in POSIX-conforming mode.
Our command should be run as sh -c "cmd..." . Let's check this
.SHELLFLAGS variable:
shellflags:
echo $(.SHELLFLAGS)
$ make shellflags echo -c -c
Allright. So can we make sure what shell make is using?
shell:
echo $$SHELL
Because dollar signs are used to start 'make' variable references, if you really want a dollar sign in a target or prerequisite you must write two of them, '$$' (*note How to Use Variables: Using Variables.). If
$ make shell echo $SHELL /bin/bash
Does it drive you crazy? It should not, because sh (dash) does not
set the SHELL environment variable, and here we run bash that runs
make that runs sh to run the echo command (should be sh's builtin
echo). Let's look at the processes to confirm that.
pstree:
pstree deleuzec
$ make pstree
[...]
st---bash-+-emacs-+-aspell
| |-qutebrowser-+-QtWebEngineProc
| | |-QtWebEngineProc---QtWebEngineProc-+-7*[QtWebE+
| | | `-2*[QtWebE+
| | |-QtWebEngineProc---3*[{QtWebEngineProc}]
| | `-65*[{qutebrowser}]
| `-10*[{emacs}]
`-xterm---luit---bash---make---pstree
Oh well, actually make did run pstree directly without any shell
here… hum, well, just because it can!
5.3 Recipe Execution When it is time to execute recipes to update a target, they are executed by invoking a new sub-shell for each line of the recipe, unless the '.ONESHELL' special target is in effect (*note Using One Shell: One Shell.) (In practice, 'make' may take shortcuts that do not affect the results.)
Let's force it to run a damn shell by providing a composite command:
pstree2:
pstree ; echo $$SHELL
$ make pstree2
[...]
|-st---bash-+-emacs-+-aspell
| | |-qutebrowser-+-QtWebEngineProc
| | | |-QtWebEngineProc---QtWebEngineProc---9*+
| | | |-QtWebEngineProc---4*[{QtWebEngineProc}+
| | | `-64*[{qutebrowser}]
| | `-10*[{emacs}]
| `-xterm---luit---bash---make---sh---pstree
[...]
/bin/bash
Ahhh! It's really running sh, good!
But then what happens with echo4 does not make sense. Could it be
that it's executing the echo external command this time? Let's try:
echo7:
echo -e \\\0352
echo8:
echo -e \\\xEA
$ make echo7 echo8 echo -e \\\0352 ê echo -e \\\xEA ê
Yes it does! But at the very beginning we discovered it did not, it
was executing dash's builtin instead. Recall the echo1 recipe, it
did output -e \xEA!
echo1:
echo -e "\xEA"
What do you think?
No idea?
The fxxxing quotes, I'll tell you! Here's what's happening:
- for
echo7andecho8, these are just a command with arguments, somakedecides it can directly execute the command, without launching any shell. Thus the externalechocommand is run. - for
echo1, the quotes certainly convincemakethat this is a complex command line that needs to be executed through a shell. So it firesshand gives it the command line.shparses the command line and decides to use its builtinechocommand.
Recall the sentence in make documentation above:
In practice, 'make' may take shortcuts that do not affect the results.
In this very specific case, it actually does affect the results…
Footnotes
Shell101 quick quizz: what would be output by echo -e \xEA? Why?
From bash manual:
Enclosing characters in double quotes preserves the literal value of all characters within the quotes, with the exception of $, `, \, [...] The backslash retains its special meaning only when followed by one of the following characters: $, `, ", \,