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
}

3 thoughts on “requoting in bash”

  1. This sequence works with no loop, but even more ugly:

    # quote the args, with a marker aroung each one
    foo=$(printf ‘< %q >’ “${@}”)
    # reintroduce empty args if nothing in the middle of a marker
    foo=”${foo//<  >/< ” >}”
    # tail of a marker + head of the next one => space
    foo=”${foo// >< / }”
    # cleanup remaining half-markers on both sides of the string
    foo=”${foo#< }” ; foo=”${foo% >}”

  2. Hi TGL. Your implementation makes assumptions about the quoting method used by printf %q, for example it assumes that the result will never contain “<  >” for any possible “$@”. This might be true at the moment, but it’s an implementation detail of bash printf that is subject to change. I appreciate the effort to remove the loop, but IMHO both of my previous implementations are better since they avoid the assumption. Thanks!

  3. Yup, you’re absolutly right that my solution relies on backslashes being used for quoting. If that changes, it would break, and thus I fully agree your conclusion about it not being an option.

    Actually, I wrote this 4 lines mainly because i’ve found this problem funny, but the implementation i really prefer is your original one with no printf (it’s the easiest to read/understand imho, and sure it’s also, by far, the fastest one).

Comments are closed.