35

I was trying to backup some files via SSH but instead of tar'ing the ones I wanted I got my home folder. I did some further testing and it boils down to this:

ssh root@server /bin/sh -c "cd /boot && ls -l"

Which to my surprise lists files in /root not /boot. But if I run the entire /bin/sh command from a terminal it properly cds and prints the /boot files.

What's happening here?

  • 1
    Unless this is an example, why are you not using ssh root@server /bin/sh -c "ls -l /boot"? – demure Jun 26 '13 at 23:50
  • It is funny you asked. I tried to do that before commenting and still did not get directory listing. – unxnut Jun 26 '13 at 23:51
  • 2
    @demure because I really wanted tar, and tar /boot will include the boot path in the result, which I do not want – Ambroz Bizjak Jun 27 '13 at 0:27
30

ssh doesn't let you specify a command precisely, as you have done, as a series of arguments to be passed to execvp on the remote host. Instead it concatenates all the arguments into a string and runs them through a remote shell. This stands out as a major design flaw in ssh in my opinion... it's a well-behaved unix tool in most ways, but when it comes time to specify a command it chose to use a single monolithic string instead of an argv, like it was designed for MSDOS or something!

Since ssh will pass your command as a single string to sh -c, you don't need to provide your own sh -c. When you do, the result is

sh -c '/bin/sh -c cd /boot && ls -l'

with the original quoting lost. So the commands separated by the && are:

`/bin/sh -c cd /boot`
`ls -l`

The first of those runs a shell with the command text "cd" and $0="boot". The "cd" command completes successfully, the $0 is irrelevant, and the /bin/sh -c indicates success, then the ls -l happens.

  • 2
    While I agree it's unfortunate that ssh behave like that, if it weren't, it would have to be called sexec, not ssh. ssh is the secure version of rsh (which behaved the same in that regard) and rlogin (rsh called rlogin when not passed a command line). It's just unfortunate that it doesn't implement rexec as well. – Stéphane Chazelas Jun 27 '13 at 9:02
  • @StephaneChazelas was there ever a rexec command? It's just a C library function as far as I know. Good point though, about rsh. Being a drop-in replacement for rsh was the key to quick adoption in the early days of ssh, when everybody had tons of scripts already using rsh. – Wumpus Q. Wumbley Jun 27 '13 at 21:12
  • I definitely used it in the past. Looking now, it must have been on HPUX as not many other Unices seem to have it, I have to admit though I was under the impression it was more widespread. – Stéphane Chazelas Jun 27 '13 at 21:27
  • Thanks. This really helped, esp. since I'm tunneling with ssh. I had to do ssh -t machine1 'ssh -t machine2 "echo \"1 && 2\""' to get '1 && 2' as the output. That killed a few hours. – ghayes Aug 16 '13 at 22:00
  • If you do want to pass a command precisely, e.g. from "$@", you can use this: "`printf "%q " "$@"`" with bash's printf to quote a string or strings with shell escapes. – Sam Watkins May 28 '16 at 2:23
33

İt's a quoting issue. Ssh already runs the command you pass it in a shell. When you pass multiple parameters, they are concatenated with a space in between to build a string. So the remote command that you are running remotely is /bin/sh -c cd /boot && ls -l (no quotes, because the quotes in your command were interpreted by the local shell).

/bin/sh -c cd /boot runs /bin/sh and tells it to run the command cd and also to set $0 to /boot. Once this is done, the parent shell (the one launched by sshd) runs ls -l.

In your case, just remove the sh -c which is completely useless unless your remote shell (as indicated in /etc/passwd or other password database) does not understand this command.

ssh root@server "cd /boot && ls -l"

If you need to invoke a different shell, you must quote the remote command to protect it from expansion by the remote shell invoked by sshd. For example, if your login shell is dash and you want to run a bash command:

ssh root@server 'bash -c "cd ~bob && ls -l"'
8

I think it has more to do with how the options are getting parsed by the shell. For example, this works:

$ ssh root@server /bin/sh -c '"cd /boot && ls -l"'

This has the same issue as your command:

$ ssh root@server /bin/sh -c 'cd /boot && ls -l'

If you enable the -v switch to ssh you can see what's going on:

1st command:

debug1: Sending command: /bin/sh -c "cd /boot && ls -l"

2nd command:

debug1: Sending command: /bin/sh -c cd /boot && ls -l

Typically when sending commands through ssh you have to pay special attention to the quoting and wrap quotes within quotes as the various layers strip them away. Also don't bother sending /bin/sh.

You can do very useful thing once you understand the quoting of ssh such as the following. This will run the command on the remote server but collect the results in a file locally on the system where you ran the ssh command:

$ ssh root@server 'free -m' > /tmp/memory.status

or this, where you tar a directory on a remote server and create it on the local system:

$ ssh remotehost 'tar zcvf - SOURCEDIR' | cat > DESTFILE.tar.gz

References

3

Typically you don't have to specify what shell to use. This works:

ssh user@host "cd /boot && pwd"

But if your default shell is problematic, then just make sure you quote the entire command, including the shell invocation. Either of the following works

ssh user@host '/bin/sh -c "cd /boot && pwd"'
ssh user@host "/bin/sh -c \"cd /boot && pwd\""
  • Note that while cd /boot && pwd works in all shells of the major families (Bourne, csh, rc), /bin/sh -c "cd /boot && pwd" would not work if the remote shell is of the rc family where " is not special. – Stéphane Chazelas Jun 27 '13 at 9:09

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.