10

I have a local and a remote host, both running Ubuntu, with the shell set to bash. In my home directory, I have two files, file-1 and file-2, both on the local host, and on a remote host called remote. There are some other files in each home directory, and I want to list only files matching file-*.

Locally, these produce the expected result, file-1 file-2:

$ ls file-*
$ bash -c 'ls file-*'

But these commands return ALL files in my home directory on the remote. What's going on there?

$ ssh remote bash -c 'ls file-*'
$ ssh remote bash -c 'ls "file-*"'
$ ssh remote bash -c 'ls file-\*'
$ ssh remote bash -c 'ls "file-\*"'

I know that simply ssh remote 'ls file-*' produces the expected result, but why does ssh remote bash -c 'ls ...' seem to drop the arguments passed to ls ...? (I've also piped the output from the remotely executed ls, and it's passed along, so only the ls seems to be affected: ssh remote bash -c 'ls file-* | xargs -I {} echo "Got this: {}"'.)

  • Curious about this as well. But are you sure about ssh remote bash -c 'ls file-* | xargs -I {} echo "Got this: {}"'? I had to run ssh remote bash -c ':; ls file-* | xargs -I {} echo "Got this: {}"' (or ssh remote bash -c ': && ls file-*') to get the expected result – nohillside Jun 01 '20 at 09:47

2 Answers2

14

The command being executed on the remote host when you use ssh remote bash -c 'ls file-*' is

bash -c ls file-*

That means bash -c executes the script ls. As positional parameters, the bash -c script gets the names on the remote host matching file-* (the first of these names will be put into $0, so it's not really part of the positional parameters). The arguments won't be passed to the ls command, so all names in the directory are listed.

ssh passes the command on the the remote host for execution with one level of quotes removed (the outer set of quotes that you use on the command line). It is the shell that you invoke ssh from that removes these quotes, and ssh does not insert new quotes to separate the arguments to the remote command (as that may interfere with the quoting used by the command).

You can see this if you use ssh -v:

[...]
debug1: Sending command: bash -c ls file-*
[...]

The three other commands that you show works the same, but will only set $0 to the string file-* while not setting $1, $2, etc. for the bash -c shell.

What you may want to do is to quote the whole command:

ssh remote 'bash -c "ls file-*"'

Which, in the ssh -v debug output, gets reported as

[...]
debug1: Sending command: bash -c "ls file-*"
[...]

In short, you will have to ensure that the string that you pass as the remote command is the command you want to run after your local shell's quote removal.

You could also have used

ssh remote bash -c \"ls file-\*\"

or

ssh remote bash -c '"ls file-*"'
Kusalananda
  • 333,661
2

I think the misunderstanding is that you expect the ssh arguments to be directly executed by the server, but that's not the case. When you write:

ssh remote bash -c 'ls file-*'

The ssh client will take bash, -c and ls file-*, join them with spaces into bash -c ls file-* and send that to the server. The server will take that string, and pass it as one argument to bash -c (if that's your remote shell). That means you end up executing the equivalent of the following in the server:

bash -c 'bash -c ls file-*'

You can verify that by executing your command while straceing the ssh server:

$ sudo strace -fe trace=execve -p "$(pgrep -o sshd)" |& grep bash
[pid 794417] execve("/bin/bash", ["bash", "-c", "bash -c ls files-*"], 0x560ab66915d0 /* 13 vars */) = 0

I think what you want is:

ssh remote 'ls file-*'

So that the server executes:

bash -c 'ls file-*'
JoL
  • 4,735