requoting in bash

While working with Uberlord on the Gentoo netscripts, I had a chance to review our requoting function. Here it is:

function requote {
        local q=\'
        set -- "${@//\'/$q\'$q}"        # quote inner instances of '
        set -- "${@/#/$q}"              # add ' to start of each param
        set -- "${@/%/$q}"              # add ' to end of each param
        echo "$*"
}

The purpose of this function is to make the arguments suitable for evaluation by the shell. This happens whenever you need a single variable that will correctly evaluate to multiple arguments without distorting the original content. In the case of passing the variable to an external program, you can’t even use bash arrays, so requoting is the only option. Here’s a simple example:

connect=$(requote chat -v '' ATZ OK ATDT318714 CONNECT '' ogin: ppp word: '<pa$$w0rd!>')
pppd connect "$connect"

In this case it’s important that quoting is preserved so that the special characters in the password, including the angle brackets that could be misinterpreted as I/O redirection, are passed to the chat program safely.

Some time after writing this function, I learned about bash printf’s %q, which “means to quote the argument in a way that can be reused as shell input” (from bash built-in help). It turns out it isn’t very easy to update our requote function to use it because, right or wrong, it drops empty arguments…

$ printf "%q " one '' '$<$'; echo
one  \$\<\$ 

This is the best I could come up with for now, which unfortunately needs a bash loop. If somebody comes up with a better implementation using printf %q, I’d be interested in knowing it!

function requote {
    declare arg
    for arg; do 
        arg=$(printf '%q' "$arg")
        printf '%s ' "${arg:-''}"
    done
}