Note: my testbed is Ubuntu 18.04.2 LTS.
Few steps are required to understand the behavior.
1. Make is somewhat smart
It looks like make
determines if a shell is required. I did
strace -f -e execve make without-pipe
and the part of the output was:
[pid 17526] execve("/usr/local/sbin/printf", ["printf", "\\x66\\x6f\\x6f"], 0x5569b5a5b440 /* 67 vars */) = -1 ENOENT (No such file or directory)
[pid 17526] execve("/usr/local/bin/printf", ["printf", "\\x66\\x6f\\x6f"], 0x5569b5a5b440 /* 67 vars */) = -1 ENOENT (No such file or directory)
[pid 17526] execve("/usr/sbin/printf", ["printf", "\\x66\\x6f\\x6f"], 0x5569b5a5b440 /* 67 vars */) = -1 ENOENT (No such file or directory)
[pid 17526] execve("/usr/bin/printf", ["printf", "\\x66\\x6f\\x6f"], 0x5569b5a5b440 /* 67 vars */) = 0
So in this case make
just probes possible locations of printf
(according to $PATH
) until the tool is found.
The behavior is different in the last case. This command
strace -f -e execve make with-pipe
yields (among other lines)
[pid 17592] execve("/bin/sh", ["/bin/sh", "-c", "printf '\\x66\\x6f\\x6f' | cat"], 0x561465476440 /* 67 vars */) = 0
The tool is smart enough to tell you want to run a pipe. If you do, a shell is used. The shell is sh
.
2. The three cases use different printf
implementations
As shown above, make without-pipe
uses printf
executable available in the OS.
printf
is a builtin in bash (confirm with type -a printf
), so make with-bash-and-pipe
uses the builtin from bash.
printf
is also a builtin in sh (confirm with (unset PATH; printf 'printf works\n'; ls)
; if it wasn't a builtin, sh couldn't find the executable like it cannot find ls
), so make with-pipe
uses the builtin from sh.
3. printf
is not required to understand \x66
and such
POSIX specification of printf
says:
The format operand shall be used as the format string described in XBD File Format Notation with the following exceptions:
[…]
Neither of the two linked documents says \xHH
and such must be special. I'm not sure if the specification explicitly allows them to be special, but the fact is they are special for some printf
-s in the question.
But not for the printf
builtin in sh.
what is the proper way to make the last target produce the same output? The one with-bash-and-pipe
is a sort of workaround.
You can make sure printf
you run is not a builtin by specifying its full path:
/usr/bin/printf '\x66\x6f\x6f' | cat
however this requires you to know the path beforehand. To fix this issue you can use env
:
env printf '\x66\x6f\x6f' | cat
env
will run the executable found in $PATH
. You can expect env
to be present because it's required by POSIX.
On the other hand in general you can't be sure any printf
executable (builtin or not) does support \xHH
in the first place. Therefore the real fix is to rewrite your format string so it's properly understood by all POSIX-compliant implementations of printf
. This seems useful:
In addition to the escape sequences shown in XBD File Format Notation ( \\
, \a
, \b
, \f
, \n
, \r
, \t
, \v
), \ddd
, where ddd is a one, two, or three-digit octal number, shall be written as a byte with the numeric value specified by the octal number.
Example:
printf '\146\157\157'
This approach works in all three cases.
Thank you for the comprehensive explanation.
The only remaining question is, is there a portable way to print a byte from the hexadecimal (or decimal) representation which doesn't require a lot of extra dependencies. – Sergei Morozov – 2019-05-05T01:42:12.157