8

Is there any consistent logic to it?

some-command "${somevariable//some pattern/'how does this get parsed?'}"

I've posted some conclusions and raw tests below as an "answer" but they're not a full answer by any means. The Bash man page appears silent on the subject.

1

3 Answers 3

7

As discussed in the comments, this seems to have changed between versions of Bash. I think this is the relevant change in bash-4.3-alpha (changelog):

zz. When using the pattern substitution word expansion, bash now runs the replacement string through quote removal, since it allows quotes in that string to act as escape characters. This is not backwards compatible, so it can be disabled by setting the bash compatibility mode to 4.2.

And the description for shopt -s compat42 (online manual):

compat42
If set, bash does not process the replacement string in the pattern substitution word expansion using quote removal.

The quoting single-quotes example:

$ s=abc\'def; echo "'${s//\'/\'\\\'\'}'"
'abc'\''def'

$ shopt -s compat42
$ s=abc\'def; echo "'${s//\'/\'\\\'\'}'"
'abc\'\\'\'def'

$ bash --version | head -1
GNU bash, version 4.4.12(1)-release (x86_64-pc-linux-gnu)

Workaround: put the replacement string in a variable, and don't use quotes inside the replacement:

$ shopt -s compat42
$ qq="'\''"; s=abc\'def; echo "'${s//\'/$qq}'";
'abc'\''def'
$ qq="'\''"; s=abc\'def; echo "'${s//\'/"$qq"}'";
'abc"'\''"def'

The funny thing is, that if the expansion is unquoted, then the quotes are removed after the substitution, in all versions. That is s=abc; echo ${s/b/""} prints ac. This of course doesn't happen with other expansions, e.g. s='a""c' ; echo ${s%x} outputs a""c.

4
  • Using a variable to contain the pattern and replacement is a great suggestion! It helped me get this working: q="'"; qq="'"'"'"'"'"'"'"; cmd=${cmd//$q/$qq}; eval "bash -c '$cmd'"
    – haridsv
    Commented Nov 27, 2020 at 14:39
  • @haridsv, That looks horrible. And eval bash -c? Wouldn't just bash -c do? Or exporting a function to run in the launched shell..? Well, it wouldn't if $cmd contains variable expansions that you want to expand in the outer shell but, still, yngh.
    – ilkkachu
    Commented Nov 27, 2020 at 15:29
  • Yep , it does look horrible. I was actually testing multilevel escaping whether it would work. When I have to pass dynamically constructed commands to say, docker exec bash -c, I wrap the command in single quotes, but then what if the command itself has single quotes? The above quote replacement ensures that they are protected. I just had an instance in which first a regex would be wrapped and then the whole of the command, so I put the above in a bash function and called it multiple time to make sure that wrapping still works at multiple levels. The eval is just a mock of the original.
    – haridsv
    Commented Nov 28, 2020 at 14:57
  • @haridsv, ok, right, I guess it makes more sense with docker.
    – ilkkachu
    Commented Nov 28, 2020 at 15:18
1

General rules by reverse engineering:

  • Quotes must be coupled (completed)
  • Quotes are preserved (included in the actual replacement)
  • Backslashes are preserved if they come before an arbitrary letter
  • Backslashes are preserved if they escape a single quote
  • A backslash backslash sequence is reduced to one backslash even within single quotes
  • You can't escape a single quote within single quotes
  • Parameter expansion works inside single quotes the same as outside
  • If a dollar sign is escaped with a backslash the dollar sign is preserved literally and the backslash is removed

And a conclusion:

  • There is absolutely no way to produce the literal sequence '\'' as a substitution through parameter expansion.
  • However, it is very easy to produce the literal sequence "'\''" as a substitution.

Some raw tests follow.

[vagrant@localhost ~]$ echo "$0"
-bash
[vagrant@localhost ~]$ echo "${0//a/x}"
-bxsh
[vagrant@localhost ~]$ echo "${0//a/some long string  with spaces}"
-bsome long string  with spacessh
[vagrant@localhost ~]$ echo "${0//a/"quoted string"}"
-b"quoted string"sh
[vagrant@localhost ~]$ echo "${0//a/"unfinished quote}"
> wat
> }"
-b"unfinished quote}"
wat
sh
[vagrant@localhost ~]$ echo "${0//a/\"escaped quote}"
-b"escaped quotesh
[vagrant@localhost ~]$ echo "${0//a/\\escaped escape}"
-b\escaped escapesh
[vagrant@localhost ~]$ echo "${0//a/\'escaped single quote}"
-b\'escaped single quotesh
[vagrant@localhost ~]$ echo "${0//a/''}"
-b''sh
[vagrant@localhost ~]$ echo "${0//a/''''}"
-b''''sh
[vagrant@localhost ~]$ echo "${0//a/'''}"
> a'b}c"d
-b'''}"
a'bshcd
[vagrant@localhost ~]$ echo "${0//a/'''}"
> w'x}y"z
-b'''}"
w'xshyz
[vagrant@localhost ~]$ echo "${0//a/'\'\\"a test'\'}"
> ^C
[vagrant@localhost ~]$ echo "${0//a/'\''\\"a test'\'}"
-b'\''\"a test'\'sh
[vagrant@localhost ~]$ echo "${0//a/'\''\\"a test'\$0'}"
> ^C
[vagrant@localhost ~]$ echo "${0//a/\\"a test'\$0'}"
> w}x"y
-b\"a test'$0'}"
wshxy
[vagrant@localhost ~]$ echo "${0//a/\\\"a test'\$0'}"
-b\"a test'$0'sh
[vagrant@localhost ~]$ echo "${0//a/\\\"a test'\\'$0'}"
> ^C
[vagrant@localhost ~]$ echo "${0//a/\\\"a test'\\$0'}"
-b\"a test'\-bash'sh
[vagrant@localhost ~]$ 
12
  • I don't have time for a full answer right now, but here is a tip: Instead of testing echo "${0//bar/qux}", test temp=${0//bar/qux}; echo "$temp" (notice lack of double quotes in assignment to temp). You're fighting a very unintuitive nesting combination of two layers of quoting rules - the double quotes around an expansion as part of a command, and the quoting of strings inside the substring replacement: in a plain variable assignment q=${...} (but not in local q=${...} or export q=${...}) expansions happen as if quoted, so you can work with the substring replacement by itself.
    – mtraceur
    Commented Dec 15, 2017 at 8:13
  • But also here is a minimal example of producing the literal '\'' as part of a substitution inline/nested in a double-quoted variable expansion using gratuitous backslash escapes: q=abc\'def; echo "$q"; echo "${q//\'/\'\\\'\'}". And another minimal example using double-quotes-in-double-quotes: q=abc\'def; echo "$q"; echo "${q//"'"/"'\''"}". Like I said, the nesting rules are unintuitive, which is why I prefer the temp-variable equivalents: q=abc\'def; echo "$q"; q=${q//\'/\'\\\'\'}; echo "$q" and q=abc\'def; echo "$q"; q=${q//"'"/"'\''"}; echo "$q"
    – mtraceur
    Commented Dec 15, 2017 at 8:23
  • @mtraceur, what version of what shell are you using? With bash 4.1.2 your first version produces abc\'\\'\'def
    – Wildcard
    Commented Dec 15, 2017 at 8:31
  • 1
    But here's one that works in both 4.4 and 3.2: qq="'\''"; s=abc\'def; echo "$s"; echo "'${s//\'/$qq}'"; (i.e. put the replacement string in variable)
    – ilkkachu
    Commented Dec 15, 2017 at 9:50
  • 1
    ksh seems to work like the newer Bash, but my zsh doesn't seem to take the quotes any more specially than other characters (it probably has a flag for that anyway)... rats, you nerd-sniped me.
    – ilkkachu
    Commented Dec 15, 2017 at 10:15
0

Through experimentation, I've found the following syntax to work for converting single-quotes to double-quotes in GNU bash, version 5.0.11(1)-release (x86_64-pc-linux-gnu)...

text="aaa'bbb'ccc"; echo "${text//$"'"/$'"'}"

Result is...

aaa"bbb"ccc

Note that the single-quote pattern must be enclosed in double-quotes and the double-quote replacement enclosed in single-quotes.

There is a relevent shopt documented as...

extquote  
    If set, $'string' and $"string" quoting is performed within
    ${parameter} expansions  enclosed  in  double quotes.  This option
    is enabled by default.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .