To escape variables to be used on the left hand side and right hand side of a s
command in sed
(here $lhs
and $rhs
respectively), you'd do:
escaped_lhs=$(printf '%s\n' "$lhs" | sed 's:[][\\/.^$*]:\\&:g')
escaped_rhs=$(printf '%s\n' "$rhs" | sed 's:[\\/&]:\\&:g;$!s/$/\\/')
sed "s/$escaped_lhs/$escaped_rhs/"
Note that $lhs
cannot contain a newline character.
That is, on the LHS, escape all the regexp operators (][.^$*
), the escaping character itself (\
, and the separator (/
).
On the RHS, you only need to escape &
, the separator, backslash and the newline character (which you do by inserting a backslash at the end of each line except the last one ($!s/$/\\/
)).
That assumes you use /
as a separator in your sed
s
commands and that you don't enable Extended REs with -r
(GNU sed
/ssed
/ast
/busybox sed
) or -E
(BSDs, ast
, recent GNU, recent busybox) or PCREs with -R
(ssed
) or Augmented REs with -A
/-X
(ast
) which all have extra RE operators.
A few ground rules when dealing with arbitrary data:
- Don't use
echo
- quote your variables
- consider the impact of the locale (especially its character set: it's important that the escaping
sed
commands are run in the same locale as the sed
command using the escaped strings (and with the same sed
command) for instance)
- don't forget about the newline character (here you may want to check if
$lhs
contains any and take action).
Another option is to use perl
instead of sed
and pass the strings in the environment and use the \Q
/\E
perl
regexp operators for taking strings literally:
A="$lhs" B="$rhs" perl -pe 's/\Q$ENV{A}\E/$ENV{B}/g'
perl
(by default) will not be affected by the locale's character set as, in the above, it only considers the strings as arrays of bytes without caring about what characters (if any) they may represent for the user. With sed
, you could achieve the same by fixing the locale to C
with LC_ALL=C
for all sed
commands (though that will also affect the language of error messages, if any).
In some shells, you can also do the escaping without having to resort to external utilities.
In zsh
:
set -o extendedglob
escaped_lhs=${lhs//(#m)[][\\.^$\/&]/\\$MATCH}
escaped_rhs=${rhs//(#m)[\\&\/$'\n']/\\$MATCH}
In ksh93
:
escaped_lhs=${lhs//[][\\.^$\/&]/\\\0}
escaped_rhs=${rhs//[\\&\/$'\n']/\\\0}
In fish
3.4.0+:
set escaped_lhs (
string replace -ar -- '[][\\\\/.^$*]' '\\\\$0' "$lhs" |
string collect --allow-empty
)
set escaped_rhs (
string replace -ar -- '[\\\\&/'\n']' '\\\\$0' "$rhs" |
string collect --allow-empty --no-trim-newlines
)