after all, what could be more exciting?
okay, so where we last left off, we had something like:
# escape any single quotes in an argument
quote(){
echo "$1" | sed -e "s,','\\\\'',g"
}
# save up a properly quoted/escaped version of "$@"
for arg in "$@"; do
saved="${saved:+$saved }'$(quote "$arg")'"
done
i got some really interesting feedback from a few people; Loïc Minier who has some similar code that he cuts and pastes on an as-needed basis, and Ralf Wildenhues who has overseen some similar code that exists in the autoconf system.
a notable feature in Loïc's solution was to quote the string entirely within the escaping function, whereas in mine the result was a "quotable" string (i.e. the quotes were put around the output afterwards).
> escape() {
> echo "$*" | sed "s/'/'\"'\"'/g; s/.*/'&'/"
> }
Ralf pointed out one potential problem with both of these approaches. on the topic of portability, there are some implementations of echo that will directly interpret control characters ('\n', '\t', etc) and others not. he suggested that "printf" be used instead:
- echo "$1" | sed -e "s,','\\\\'',g"
+ printf '%s\n' "$1" | sed -e "s,','\\\\'',g"
though there's no reason i can think of not to take that a bit further and use a herestring, which could save an extra fork if printf isn't a builtin:
quote(){
sed -e "s,','\\\\'',g" << EOF
$1
EOF
}
also, Ralf suggested in the interest of efficiency that some kind of case statement be used to avoid unnecessary forks when an argument doesn't contain any "'" characters. this is apparently a trick used in autoconf-generated scripts:
case "$arg" in
*\'*)
saved="${saved:+$saved }'$(quote "$arg")'"
;;
*)
saved="${saved:+$saved }'$arg'"
;;
esac
finally, a lingering problem which hasn't been solved on the autoconf side of things (nor in my previous solution) was that in the case that an argument contained trailing newlines, such whitespace would be silently discarded in the quoted version (this is due to how shell command substitution works in general). but really, this is quite the corner case... how often does one run a command line with both an escaped quote and trailing newlines in an argument? an example:
rangda[/home/sean] ./good.sh "isn't very common" "to give args" "with trailing \\n's:
"
original command line, 3 arguments: isn't very common to give args with trailing \n's:
arg: isn't very common
arg: to give args
arg: with trailing \n's:
mangled command line, 0 arguments:
restored command line, 3 arguments: isn't very common to give args with trailing \n's:
arg: isn't very common
arg: to give args
arg: with trailing \n's:
rangda[/home/sean] :)
however, i noticed that a slightly modified version of Loïc's approach might not suffer from this problem because the string would be quoted before being returned (thus any newlines would be enclosed in quotes that would preserve them). I mentioned this to Ralf who will be bringing it up on the autoconf-patches mailing list... sounds promising anyway. so i give you the combined improved version:
#!/bin/sh -e
# escape any single quotes in an argument and then quote it
quote(){
sed -e "s,','\\\\'',g; 1s,^,',; \$s,\$,',;" << EOF
$1
EOF
}
# save up a properly quoted/escaped version of "$@"
for arg in "$@"; do
case "$arg" in
# when a string contains a "'" we have to escape it
*\'*)
saved="${saved:+$saved }$(quote "$arg")"
;;
# otherwise just quote the variable
*)
saved="${saved:+$saved }'$arg'"
;;
esac
done
echo original command line, $# arguments: "$@"
while test -n "$*"; do echo arg: "$1"; shift; done
echo mangled command line, $# arguments: "$@"
# restore the cmdline
eval set -- "$saved"
echo restored command line, $# arguments: "$@"
while test -n "$*"; do echo arg: "$1"; shift; done
output from the same example cmdline:
rangda[/home/sean] ./better.sh "isn't very common" "to give args" "with trailing \\n's:
"
original command line, 3 arguments: isn't very common to give args with trailing \n's:
arg: isn't very common
arg: to give args
arg: with trailing \n's:
mangled command line, 0 arguments:
restored command line, 3 arguments: isn't very common to give args with trailing \n's:
arg: isn't very common
arg: to give args
arg: with trailing \n's:
rangda[/home/sean] :)