I am asking why these three commands give three different answers:
$ printf "%s\n" `echo ../.. | sed 's/[.]/\\&/g'`
&&/&&
$ printf "%s\n" $(echo ../.. | sed 's/[.]/\\&/g')
../..
$ printf "%s\n" "$(echo ../.. | sed 's/[.]/\\&/g')"
\.\./\.\.
I am asking why these three commands give three different answers:
$ printf "%s\n" `echo ../.. | sed 's/[.]/\\&/g'`
&&/&&
$ printf "%s\n" $(echo ../.. | sed 's/[.]/\\&/g')
../..
$ printf "%s\n" "$(echo ../.. | sed 's/[.]/\\&/g')"
\.\./\.\.
This is a tough question. The easiest example to explain in the last one.
Well, actually, a fully quoted example:
$ printf "%s\n" "$( echo "../.." | sed 's/[.]/\\&/g' )"
\.\./\.\.
Here there are no tricks nor changes done by the shell because everything is quoted. The most internal echo "../.."
is quoted and therefore not subject to filename expansion. It goes to the sed unchanged and sed changes each dot to a \&
. Then the result of that command is also quoted "$(...)"
which (again) avoids any change by the shell and the printf
command also prints \.\./\.\.
. No surprises here.
If there is filename expansion (globbing) over ../..
we end with ../..
anyway. So, the end string is the same. But lets test this issue:
$ echo ../../t*
../../test2.txt ../../tested ../../test.txt
$ set -f # remove all filename expansion
$ echo ../../t* ../..
../../t* ../..
And, anyway, an string that doesn't contain either a *
, a ?
, or a [
is not subject to Pattern Matching Notation
Proof: Set GLOBIGNORE
$ (GLOBIGNORE='.*:..*'; echo "any" ../../t* ../.. "value")
any ../.. value
If ../..
were subject to globbing (filename expansion) then it would be removed due to the value of GLOBIGNORE.
However, to be sure that there is no filename expansion we can switch to a (most probably) non-existent filename: ##/##
There is no reason that the shell should remove a \
even if (the command execution is) unquoted:
$ printf '%s\n' $(printf '%s\n' '\#\#/\#\#')
\#\#/\#\#
In fact, none of the shells I tested shows what you report in your second example. (please correct me!).
EDIT: In bash 5.0.17 there is a bug. But only with the dot.
$ b50sh
$ echo $BASH_VERSION
5.0.17(3)-release
$ printf '%s\n' $(printf '%s\n' '../..')
../..
$ printf '%s\n' $(printf '%s\n' '##/##')
##/##
$ printf '%s\n' $(printf '%s\n' '>>/>>')
>>/>>
$ exit
$ echo $BASH_VERSION
5.1.4(1)-release
$ printf '%s\n' $(printf '%s\n' '../..')
../..
$ printf '%s\n' $(printf '%s\n' '##/##')
##/##
And seems that it has been solved in bash 5.1.4
Here is the trickiest issue: Inside backticks every \\
becomes a \
.
So, the sed command (as seen by sed) is: s/[#]/\&/g
. To do what I believe you meant you need to duplicate the \
s:
$ printf '%s\n' `printf '%s\n' '##/##' | sed 's/[#]/\\&/g'`
&&/&&
$ printf '%s\n' "printf '%s\n' '##/##' | sed 's/[#]/\\&/g'
"
&&/&&
$ printf '%s\n' "printf '%s\n' '##/##' | sed 's/[#]/\\\\&/g'
"
##/##
In bash version 5.0.17, man bash
notes the following distinction between the two forms of command substitution:
When the old-style backquote form of substitution is used, backslash retains its literal meaning except when followed by $, `, or \. The first backquote not preceded by a backslash terminates the command sub‐ stitution. When using the $(command) form, all characters between the parentheses make up the command; none are treated specially.
You can see the difference if you set -x
in the shell:
$ set -x
$ printf "%s\n" echo ../.. | sed 's/[.]/\\&/g'
++ echo ../..
++ sed 's/[.]/&/g'
- printf '%s\n' '&&/&&'
&&/&&
shows \\&
is collapsed to \&
(because \
is followed by \
); the &
loses its special meaning in sed, so ..
becomes &&
, whereas (new style)
$ printf "%s\n" $(echo ../.. | sed 's/[.]/\\&/g')
++ echo ../..
++ sed 's/[.]/\\&/g'
+ printf '%s\n' ../..
../..
both backslashes are passed literally to sed, which replaces ..
by \.\.
(\\
is a literal backslash, and &
retains its special meaning); when the unquoted result is then echoed by the shell, you get ../..
while the quoted command substitution prints the sed output \.\./\.\.
verbatim:
$ printf "%s\n" "$(echo ../.. | sed 's/[.]/\\&/g')"
++ echo ../..
++ sed 's/[.]/\\&/g'
+ printf '%s\n' '\.\./\.\.'
\.\./\.\.
See also Why is $(...) preferred over ...
(backticks)?
In bash version 4.4.20, both the unquoted and quoted versions of the $(...)
command substitution give the same result:
$ echo $BASH_VERSION
4.4.20(1)-release
$ set -x
$ printf "%s\n" $(echo ../.. | sed 's/[.]/\&/g')
++ echo ../..
++ sed 's/[.]/\&/g'
- printf '%s\n' '../..'
../..
bash
5.0, a backslash that occurs in an unquoted expansion is taken as a glob operator even when not followed by *?[
. POSIX was discussing making it a requirement and bash ended up implementing it. Thankfully, I eventually managed to get POSIX to drop that idea and settle for something more reasonable and bash reverted its behaviour in 5.1. See discussion around this and https://www.austingroupbugs.net/view.php?id=1234/msg04139.html
– Stéphane Chazelas
Mar 26 '22 at 14:12