Search code examples
bashshellargumentsposixquoting

How can `printf %q` be reversed in a shell?


The command printf %q (from GNU coreutils or bash) can be used to quote an array of arguments including whitespaces, newlines and quotes. For example:

$ main() { printf '%q ' "${@}"; } && main "'a'" '"b"' 'c d' $'e\nf'
\'a\' \"b\" c\ d $'e\nf' 

Is it possible to reverse this operation, i.e. create an argument array from a string created by printf %q

  1. In a POSIX shell?
  2. In Bash?
  3. Using additional tools?
Use case for this question

An "argument provider" uses printf %q "${@}" to wrap a list of arguments in a single string. The arguments may hold arbitrary content, including but not limited to: quotes, quoted strings with newlines, strings that have been created by printf %q. A shell script should be used to unwrap the string into an argument array. Unwrapping should avoid eval and unquoted variables. The string may be handed over either as command line argument or by stdout/stdin.

Insights
  • xargs does not handle quoted newlines.
  • zsh can do this for a string stored in variable args: "${(Q@)${(z)args}}"

Solution

  • use declare:

    quoted=$(main  "'a'" '"b"' 'c d' $'e\nf')
    declare -a "myvar=($quoted)"
    declare -p myvar
    

    outputs

    declare -a myvar=([0]="'a'" [1]="\"b\"" [2]="c d" [3]=$'e\nf')
    

    This can't be used to evaluate commands:

    $ declare -a "myvar=($(main  ls -l sensitive files))"
    $ declare -p myvar
    declare -a myvar=([0]="ls" [1]="-l" [2]="sensitive" [3]="files")
    

    One thing to note: if you use declare in a function, the variable becomes local to that function (unless you use declare -g in which case it's global)