Colin Watson: SSH quoting

A while back there was a thread on one of our company mailing lists about
SSH quoting, and I posted a long answer to it. Since then a few people have
asked me questions that caused me to reach for it, so I thought it might be
helpful if I were to anonymize the original question and post my answer here.

The question was why a sequence of commands involving ssh and fiddly
quoting produced the output they did. The first example was this:

$ ssh user@machine.local bash -lc "cd /tmp;pwd"

Oh hi, my dubious life choices have been such that this is my specialist subject!

This is because SSH command-line parsing is not quite what you expect.

First, recall that your local shell will apply its usual parsing, and the
actual OS-level execution of ssh will be like this:

[0]: ssh
[1]: user@machine.local
[2]: bash
[3]: -lc
[4]: cd /tmp;pwd

Now, the SSH wire protocol only takes a single string as the command, with
the expectation that it should be passed to a shell by the remote end. The
OpenSSH client deals with this by taking all its arguments after things like
options and the target, which in this case are:

[0]: bash
[1]: -lc
[2]: cd /tmp;pwd

It then joins them with a single space:

bash -lc cd /tmp;pwd

This is passed as a string to the server, which then passes that entire
string to a shell for evaluation, so as if you’d typed this directly on the server:

sh -c 'bash -lc cd /tmp;pwd'

The shell then parses this as two commands:

bash -lc cd /tmp

The directory change thus happens in a subshell (actually it doesn’t quite
even do that, because bash -lc cd /tmp in fact ends up just calling cd
because of the way bash -c parses multiple arguments), and then that
subshell exits, then pwd is called in the outer shell which still has the
original working directory.

The second example was this:

$ ssh user@machine.local bash -lc "pwd;cd /tmp;pwd"

Following the logic above, this ends up as if you’d run this on the server:

sh -c 'bash -lc pwd; cd /tmp; pwd'

The third example was this:

$ ssh user@machine.local bash -lc "cd /tmp;cd /tmp;pwd"

And this is as if you’d run:

sh -c 'bash -lc cd /tmp; cd /tmp; pwd'

Now, I wouldn’t have implemented the SSH client this way, because I agree
that it’s confusing. But /usr/bin/ssh is used as a transport for other
things so much that changing its behaviour now would be enormously
disruptive, so it’s probably impossible to fix. (I have occasionally
agitated on openssh-unix-dev@ for at least documenting this better, but
haven’t made much headway yet; I need to get round to preparing a
documentation patch.) Once you know about it you can use the proper
quoting, though. In this case that would simply be:

ssh user@machine.local 'cd /tmp;pwd'

Or if you do need to specifically invoke bash -l there for some reason
(I’m assuming that the original example was reduced from something more
complicated), then you can minimise your confusion by passing the whole
thing as a single string in the form you want the remote sh -c to see, in
a way that ensures that the quotes are preserved and sent to the server
rather than being removed by your local shell:

ssh user@machine.local 'bash -lc "cd /tmp;pwd"'

Shell parsing is hard.

Go to Source of this post
Author Of this post:
Title Of post: Colin Watson: SSH quoting
Author Link: {authorlink}