1

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')"
\.\./\.\.
ilkkachu
  • 138,973
eracesa
  • 13

2 Answers2

4

This is a tough question. The easiest example to explain in the last one.

Fully quoted

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.

Change to ##/##

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: ##/##

Unquoted command execution

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

Backticks

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'" ##/##

4

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' '../..'

../..

steeldriver
  • 81,074
  • @IsaaC after which sed command? – steeldriver Mar 12 '22 at 19:19
  • @IsaaC ah so it seems to be a bash version thing - I get exactly what I posted above in bash 5.0.17, but what you posted in bash 4.4.20 ... – steeldriver Mar 12 '22 at 19:27
  • Actually, I am in bash 5.1.4-release(1). And yes, 5.0.x seems to have a bug in there. –  Mar 12 '22 at 19:33
  • In 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
  • 1
    @ilkkachu Looks like I mangled the two URLs. Should be https://www.mail-archive.com/austin-group-l@opengroup.org/msg04139.html and https://www.austingroupbugs.net/view.php?id=1234 – Stéphane Chazelas Mar 26 '22 at 14:56