2

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.

  • I thought I might indirect through an array and use declare instead of eval. But that's just as evil. And so is local. – starfry Nov 25 '16 at 18:37
1

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}"

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.