4

Similar to Why doesn't echo called as /bin/sh -c echo foo output anything? but using ssh.

Why doesn't this output anything:

ssh localhost sh -c 'echo "0=$0 1=$1"' arg1 arg2 arg3

But if I change it to

ssh localhost sh -c 'true; echo "0=$0 1=$1"' arg1 arg2 arg3
# Output is
0=bash 1= arg1 arg2 arg3

I see behavior that implies the echo command is being run but the way the variable substitution is working is not as expected. See https://unix.stackexchange.com/a/253424/119816

Running without ssh works as expected

Same as above but removing the ssh command

sh -c 'echo "0=$0 1=$1"' arg1 arg2 arg3
# Output is
0=arg1 1=arg2

Adding true gives same output

sh -c 'true; echo "0=$0 1=$1"' arg1 arg2 arg3 0=arg1 1=arg2

I'm trying copy a root accessible file from one system to another

I'm trying to get an ssh command working to copy a root file to another system. The command I'm trying is:

file=/etc/hosts
set -x
ssh host1 sudo cat $file | ssh host2 sudo sh -c 'exec cat "$0"' $file
# Output is
+ ssh host2 sudo sh -c 'exec cat > "$0"' /etc/hosts
+ ssh host1 sudo cat /etc/hosts
bash: : No such file or directory

This looks OK to me, and I'm not sure how else to troubleshoot.

My solution

My solution is to fall back to what I've used before

ssh host1 sudo cat $file | ssh host2 sudo tee $file > /dev/null

The above works.

Searching for a solution

I've had this problem and asked the question:

Other's have had problems/questions with sh -c command:

But there must be a subtlety that occurs when using ssh which results in an additional shell evaluation.

Toby Speight
  • 8,678
PatS
  • 604

2 Answers2

11

It's the same issue as in your referenced question. The local shell removes a layer of quotes. Here's what's happening

Initial code

ssh localhost sh -c 'echo "0=$0 1=$1"' arg1 arg2 arg3

After local shell expansion but before command execution

ssh localhost sh -c echo "0=$0 1=$1" arg1 arg2 arg3
#                   ^^^^^^^^^^^^^^^^ a single token
#             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ passed to the remote shell

In the remote shell after shell expansion but before execution

sh -c echo 0= 1= arg1 arg2 arg3
#          ^^^^^ a single token
#     ^^^^ argument for -c

The echo gets bound to the sh -c as the command to execute and the remainder are arguments.

You should get a blank line.

Personally, I learned that ssh passed its args directly to the remote shell, and that's where it was executed. As a result I tend to write ssh commands like this, which reminds me that the some command line… part will be passed verbatim to a remote shell for execution as if I'd typed it directly:

ssh remoteHost 'some command line…'

(Wrap the line in single quotes to avoid local evaluation, or use double quotes to interpolate local variables.)


You then say that actually you're trying to get an ssh command working to copy a root file to another system

file=/etc/hosts
ssh -n host1 "scp -p '$file' host2:'/path/to/destination'"

Or, since the file name is safe, and assuming you have root equivalence between the local client and host1, and from host1 to host2:

ssh -n root@host1 scp -p /etc/hosts host2:/etc/hosts

With a modern scp that in turn can be simplified further:

scp -OR -p root@host1:/etc/hosts root@host2:/etc/
Chris Davies
  • 116,213
  • 16
  • 160
  • 287
  • obvious followup: why the heck does ssh concatenate all the arguments and parse them again? It already has the separate arguments - this is annoying behaviour. – user253751 Sep 16 '22 at 07:43
  • 2
    To be honest I'd never considered that. I learned that ssh passed its args to the remote shell, and that's where it was executed. As a result I tend to write ssh remoteHost 'some command line…', which reminds me that the some command line… part will be passed verbatim to a remote shell for execution as if I'd typed it directly. (Wrap the line in single quotes to avoid local evaluation, or use double quotes to interpolate local variables.) I'll update my question to include this. – Chris Davies Sep 16 '22 at 09:01
  • 1
    @user253751 ssh doesn't parse the arguments again, it passes them to a shell on the remote system and that parses them. If the remote command wasn't parsed by a remote shell, you couldn't use any shell features like redirects, wildcards, or compound commands (multiple commands separated by ;, ||, &&, or if or while or ...). It looks to me like the real problem here is that you've added a third shell (and a third layer of parsing) by using sh -c. – Gordon Davisson Sep 16 '22 at 09:29
  • @GordonDavisson why should one expect to be able to use shell features in a command executed by ssh? by default they would be executed by the local shell, and if you want a remote shell, then you ask for one. – user253751 Sep 16 '22 at 09:52
  • 3
    @user253751 ssh does ask for a remote shell. By definition – Chris Davies Sep 16 '22 at 11:09
  • @user253751 To be fair "ssh" stands for "secure shell" – gronostaj Sep 16 '22 at 14:41
  • 1
    @user253751, the remote system might implement some restrictions through the choice of the user's shell, including things like having a restricted or totally customized shell, or even preventing logins by having something like /bin/false as the shell. That's useful and straightforward behaviour and means restrictions like that don't need to be implemented in multiple places (like the SSH server configuration in addition to the user's passwd entry). I wouldn't be surprised if there were compatibility issues with earlier tools like rsh too, but I don't really know about those. – ilkkachu Sep 16 '22 at 14:48
  • @gronostaj Yes, that's what it stands for, but the intent is that it's the secure replacement for the previous rsh, which stands for "remote shell". They could have called it srsh to be explicit, but they chose a shorter name. – Barmar Sep 16 '22 at 14:56
  • @user253751 why the heck does ssh concatenate all the arguments?. So that you can write simple commands more easily, e.g. ssh hostname cp foo bar instead of ssh hostname 'cp foo bar' – Barmar Sep 16 '22 at 14:58
  • 1
    ssh passes the arguments as a single token. concatenation would be: sh-c'echo"0=$01=$2"'arg1arg2arg3 . perhaps i'm splitting hair here, but I would not call what ssh is doing concatenation. – Ярослав Рахматуллин Sep 16 '22 at 17:27
  • In my case I could not use root directly to login, this was not permitted. All root actions had to be initiated via sudo after logging in as a "non root" user. So the statement "assuming you have root equivalence" and corresponding use of root@host1:/etc/hosts could not be used. Also it took me a little while to understand your answer, but overall it was very informative. Thanks and Best Wishes. – PatS Sep 22 '22 at 16:16
5

If you add -v to your ssh command, it will show you what it's doing:

debug1: Sending command: sh -c echo "0=$0 1=$1" arg1 arg2 arg3

Sure enough, if we run that sh command locally, we just get an empty line of output, because sh is running echo and "0=$0 1=$1" becomes the zeroth argument.

When we prepend true;, this happens:

debug1: Sending command: sh -c true; echo "0=$0 1=$1" arg1 arg2 arg3

Now sh just runs true. And then echo outputs its arguments:

0=bash 1= arg1 arg2 arg3

What you probably want is to run sh -c 'echo "0=$0 1=$1"' arg1 arg2 arg3 on the far side. You'll need to quote that whole command so it's received intact by the remote shell:

ssh localhost "sh -c 'echo \"0=\$0 1=\$1\"' arg1 arg2 arg3"

Output:

0=arg1 1=arg2

If you have GNU printf (on the local side), you could make this more readable by using it to format the command string suitably for the far end:

command='echo "0=$0 1=$1"'
ssh localhost "sh -c $(printf %q "$command") arg1 arg2 arg3"
Toby Speight
  • 8,678
  • 1
    Pro Tip: Adding -v option. I use it all the time to debug ssh credential issues, but never thought to use it for debugging shell commands. – PatS Sep 22 '22 at 16:18