I would like to put this command into a file to be run later:

ln -s "$xr"/ya.txt ~

I can do that with (1):

cat > zu.sh <<eof
ln -s "$xr"/ya.txt ~

or (2):

printf 'ln -s "%s"/ya.txt ~\n' "$xr" > zu.sh

or (3):

echo "ln -s '$xr'/ya.txt ~" > zu.sh

or (4):

printf 'ln -s %q/ya.txt ~\n' "$xr" > zu.sh

or (5):

printf 'ln -s "%s"/ya.txt ~\n' "${xr//\"/\\\"}"

however each solution is problematic. if the variable in (1) or (2) contains a double quote, they will fail; if the variable in (3) contains a single quote, it will fail. (4) is good but is not defined by POSIX. (5) is good but is a Bashism. it looks like the best option would be to use (1) or (2) and escape any double quotes in the variable, but can that be done another way?


I think this is safe:

esc() {
    printf "%s\n" "$1" | sed -e "s/'/'\"'\"'/g" -e "1s/^/'/" -e "\$s/\$/'/"

It single-quotes the string, so that any $, `, \, and " in the input string don't matter, and turns any existing ' characters into '"'"' (i.e. end single-quoting, double-quote a lone single-quote, then reënter single-quoting).

It was tempting to use $(...) command substitution in there, except that then it will eat any trailing newlines in the input. Instead the opening and closing quotes are inserted by the second and third sed scripts themselves, at the start of the first line and the end of the last line. Any embedded newlines are left raw, which is fine.

The output is suitable for copying back into the shell, even in the most pathological case I can invent (using Bash ANSI-C quoting $'...' to create the test string in the first place, but not afterwards):

bash-4.4$ esc $'abc\ndef ghi\'jkl$mno`pqr\\stu\\\'vwx\n\n\n'
def ghi'"'"'jkl$mno`pqr\stu\'"'"'vwx

bash-4.4$ echo 'abc
> def ghi'"'"'jkl$mno`pqr\stu\'"'"'vwx
> '
def ghi'jkl$mno`pqr\stu\'vwx

bash-4.4$ dash
$ echo 'abc
def ghi'"'"'jkl$mno`pqr\stu\'"'"'vwx

'> > > >
def ghi'jkl$mno`pqr\stu\'vwx


It's safe to put that into a variable xr=$(esc "$xr") and then use that in ordinary substitution later on within your here-document or elsewhere.


Using Awk library svnpenn/stdlib, tests with quotes, backslash, dollar sign and backtick all pass:

$ awklib 'BEGIN {printf "ln -s %s/ya.txt ~\n", sh_escape(ARGV[1])}' "2'2"
ln -s 2\'2/ya.txt ~

$ awklib 'BEGIN {printf "ln -s %s/ya.txt ~\n", sh_escape(ARGV[1])}' '2"2'
ln -s '2"2'/ya.txt ~

$ awklib 'BEGIN {printf "ln -s %s/ya.txt ~\n", sh_escape(ARGV[1])}' '2\2'
ln -s '2\2'/ya.txt ~

$ awklib 'BEGIN {printf "ln -s %s/ya.txt ~\n", sh_escape(ARGV[1])}' '2$2'
ln -s '2$2'/ya.txt ~

$ awklib 'BEGIN {printf "ln -s %s/ya.txt ~\n", sh_escape(ARGV[1])}' '2`2'
ln -s '2`2'/ya.txt ~

Disclaimer: I wrote this library, but I can delete this answer if better option exists.

  • You'd want to quote everything with single quotes (except for the single quotes obviously), and not make exceptions for alnums. There are some alnums in some locales that contain the encoding of backtick or backslash for instance. You'd want to fix the locale to C as well to minimise the chances of some implementations choking on some invalid multi-byte sequences. See the shquote links I gave in a comment to your question. – Stéphane Chazelas Feb 8 '18 at 17:06
  • It also depends which shell you want to target. If you want to target all Bourne-like shells, you'd want to quote = and % as well which are special to zsh under some circumstance. The encoding of @ and _, a-zA-Z0-9 is also found including in some multibyte characters. – Stéphane Chazelas Feb 8 '18 at 17:22
  • Try for instance: awklib 'BEGIN{print "echo "sh_escape(ARGV[1])}' '=ls' | zsh – Stéphane Chazelas Feb 8 '18 at 17:27
  • 1
    Or: LC_ALL=zh_HK.big5hkscs awklib 'BEGIN{print "echo "sh_escape(ARGV[1])}' $'\xa3\\\'' | sh – Stéphane Chazelas Feb 8 '18 at 17:31
  • 1
    The problem is that by not quoting everything, someone using that function could introduce arbitrary code execution vulnerabilities on systems where those locales are available. Note that on Lubuntu or any other current GNU system, those locales may not be enabled by default but one can enable them with dpkg-reconfigure locales or equivalent (they are supported GNU locales). That concerns the BIG5, BIG5-HKSCS and GB18030 multi-byte character sets (at least). – Stéphane Chazelas Feb 9 '18 at 11:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.