First of all: I know that the easiest way to print an escaped version of a string of course would be
printf "%q" "$arg"
But I don't consider the output generally "human friendly to read".
Background: As a side project I am currently writing a tiny testing framework in bash and since it requires writing strings that can be evald, it is sometimes a bit tedious to get the quoting right. The more important it is to me that the expect function returns nice human readable errors on parsing errors. E.g. on a parsing error I want it to print
usage: expect <command> not to contain <string>
E.g.:
expect 'echo hi' not to contain 'ho'
but got
expect "echo '\$1'" not to contain '"""text"""' asdffdsa
So it should show the arguments it got escaped (if needed), but in the nicest possible way. So I wrote a function to "pretty escape" arguments (for a given definition of "pretty") that is like this:
- if it does not need any escaping: print it as is
- else use whatever is shorter to wrap (
"/') and escape the corresponding characters as needed
And while technically the printf solution is equivalent to this, I find all the escaped \", \ and so on absolutely horrific to read.
My solution is a bit complex though:
pretty_escape() {
while [ $# -gt 0 ]; do
if ! [[ "$(printf "%q" "$1")" == *\\* ]]; then # No special characters to escape, so print raw
printf "%s" "$1"
else
# calculate how many characters are added by each version of quoting compared to the other one
num_single_quote_escapes=$(($(tr -dc "'" <<< "$1" | wc -m) * 3)) # three extra chars for '\''
# NOTE: "!" can, but does not have to be special - we stick to the safe side and assume it is
# See: https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html
# special chars: $`"\!
num_double_quote_escapes=$(tr -dc '$`"\!' <<< "$1" | wc -m) # a single backslash per escape
if [ $num_single_quote_escapes -gt $num_double_quote_escapes ]; then
printf '"%s"' "$(sed -r 's/([$`"\!])/\\\1/g' <<< "$1")"
else
printf "'%s'" "$(sed -r "s/'/'"'\\'"''/g" <<< "$1")"
fi
fi
shift
# join the individual arguments with a space
[ $# -eq 0 ] || printf " "
done
}
And then
pretty_escape expect "$command" ${not} to contain "$@"
would result in the last line of my example above.
I know that it lacks a few things:
- it certainly is not the most efficient way to implement this - e.g. my use of
printf "%q"just to search for any special character is probably quite resource intensive compared to other solutions... - it does not always print the shortest possible escape sequence (e.g. one could split a string where the first half is shorter to escape with double- and the second half is shorter to escape with single quotes - example: it outputs
"\"\"\"\"'''", while'""""'"'''"would be shorter), but I am okay with that (though I would not mind it either) - it might in some situations even be harder to read - I don't like that I have to implement this myself, since that means a) it is slow, b) it is less portable, because it requires all used programs to have the needed features and even worse c) there is great potential for errors in the implementation
In some comment of some question (which I unfortunately can't find any more to give it credit) I found a reference to bash-completion but they also only do it the single quote way AFAICT.
Is it possible to achieve pretty escaping in a nicer way? Or at least something roughly similar? (I would even consider a different language/shell if it helps to simplify it)
Thanks a lot!
P.S.: Don't judge me on my maybe sometimes maybe unnecessary quoting of variables - I do it basically everywhere where I am not 100% sure that I don't want it out of pure horror by the shell syntax... Also I am always grateful for tips on how to achieve things in a nicer (or even just different) way!