Bash – Can bash expand a quoted and/or escaped string variable into words

bashbash-expansionevalshellstring

I have a bash shell variable containing a string formed of multiple words delimited by whitespace. The string can contain escapes, such as escaped whitespace within a word. Words containing whitespace may alternatively be quoted.

A shell variable that is used unquoted ($FOO instead of "$FOO") becomes multiple words but quotes and escapes in the original string have no effect.

How can a string be split into words, giving consideration to quoted and escaped characters?

Background

A server offers restricted access over ssh using the ForceCommand option in the sshd_config file to force execution of a script regardless of the command-line given to the ssh client.

The script uses the variable SSH_ORIGINAL_COMMAND (which is a string, set by ssh, that contains the command-line provided to the ssh client) to set its argument list before proceeding. So, a user doing

$ ssh some_server foo 'bar car' baz

will see the script execute and it will have SSH_ORIGINAL_COMMAND set to foo bar car baz which would become four arguments when the script does

set -- ${SSH_ORIGINAL_COMMAND}

Not the desired result. So the user tries again:

$ ssh some_server foo bar\ car baz

Same result – the backslash in the second argument needs to be escaped for the client's shell so ssh sees it. What about these:

$ ssh some_server foo 'bar\ car' baz
$ ssh some_server foo bar\\ car baz

Both work, as would a printf "%q" quoting wrapper that can simplify the client-side quoting.

Client-side quoting allows ssh to send the correctly quoted string to the server so that it receives SSH_ORIGINAL_COMMAND with the backslash intact: foo bar\ car baz.

However there is still a problem because set does not consider the quoting or escaping. There is a solution:

eval set -- ${SSH_ORIGINAL_COMMAND}

but it is unacceptable. Consider

$ ssh some_server \; /bin/sh -i

Very undesirable: eval can't be used because the input can't be controlled.

What is required is the string expansion capability of eval without the execution part.

Best Answer

Use read:

read -a ssh_args <<< "${SSH_ORIGINAL_COMMAND}"
set -- "${ssh_args[@]}"

This will parse words from SSH_ORIGINAL_COMMAND into the array ssh_args, treating backslash (\) as an escape character. The array elements are then given as arguments to set. It works with an argument list passed through ssh like this:

$ ssh some_server foo 'bar\ car' baz
$ ssh some_server foo bar\\ car baz

A printf "%q" quoting ssh wrapper allows these:

$ sshwrap some_server foo bar\ car baz
$ sshwrap some_server foo 'bar car' baz

Here is such a wrapper example:

#!/bin/bash
h=$1; shift
QUOTE_ARGS=''
for ARG in "$@"
do
  ARG=$(printf "%q" "$ARG")
  QUOTE_ARGS="${QUOTE_ARGS} $ARG"
done
ssh "$h" "${QUOTE_ARGS}"
Related Question