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.

2

2 Answers 2

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/
11
  • 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. Sep 16, 2022 at 9: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. Sep 16, 2022 at 9:29
  • 3
    @user253751 ssh does ask for a remote shell. By definition Sep 16, 2022 at 11:09
  • 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, 2022 at 14:48
  • 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, 2022 at 17:27
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"
1
  • 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, 2022 at 16:18

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .