17

Consider this:

$ ssh localhost bash -c 'export foo=bar'
terdon@localhost's password: 
declare -x DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus"
declare -x HOME="/home/terdon"
declare -x LOGNAME="terdon"
declare -x MAIL="/var/spool/mail/terdon"
declare -x OLDPWD
declare -x PATH="/usr/bin:/bin:/usr/sbin:/sbin"
declare -x PWD="/home/terdon"
declare -x SHELL="/bin/bash"
declare -x SHLVL="2"
declare -x SSH_CLIENT="::1 55858 22"
declare -x SSH_CONNECTION="::1 55858 ::1 22"
declare -x USER="terdon"
declare -x XDG_RUNTIME_DIR="/run/user/1000"
declare -x XDG_SESSION_ID="c5"
declare -x _="/usr/bin/bash"

Why does exporting a variable within a bash -c session run via ssh result in that list of declare -x commands (the list of currently exported variables, as far as I can tell)?

Running the same thing without the bash -c doesn't do that:

$ ssh localhost  'export foo=bar'
terdon@localhost's password: 
$

Nor does it happen if we don't export:

$ ssh localhost bash -c 'foo=bar'
terdon@localhost's password: 
$ 

I tested this by sshing from one Ubuntu machine to another (both running bash 4.3.11) and on an Arch machine, sshing to itself as shown above (bash version 4.4.5).

What's going on here? Why does exporting a variable inside a bash -c call produce this output?

terdon
  • 242,166
  • This doesn't answer the question, but the output is the result of running export. Zsh does the same thing. – Stephen Kitt Jan 10 '17 at 13:33
  • @StephenKitt yes, I know it's export, I'm trying to understand what's happening. I'll edit to clarify that this only happens when exporting. – terdon Jan 10 '17 at 13:39
  • Ah OK, I'd read "the list of currently exported variables, as far as I can tell" as meaning that you didn't know where the output was coming from. – Stephen Kitt Jan 10 '17 at 13:40
  • @StephenKitt I mean I'm not sure if that's every exported variable or a specific subset or what. Oh! You mean it's the output of export run alone? That I hadn't understood. – terdon Jan 10 '17 at 13:41
  • Note that foo=bar doesn't appear in the list. – deltab Jan 10 '17 at 17:46
  • @deltab no, of course not, because that was never exported. That's precisely the issue as I learned. – terdon Jan 10 '17 at 17:46

2 Answers2

30

When you run a command through ssh, it is run by calling your $SHELL with the -c flag:

-c    If the -c option is present, then commands are read from 
      the first non-option argument command_string.  If there  are
      arguments  after the command_string, the first argument is 
      assigned to $0 and any remaining arguments are assigned to
      the positional parameters.  

So, ssh remote_host "bash -c foo" will actually run:

/bin/your_shell -c 'bash -c foo'

Now, because the command you are running (export foo=bar) contains spaces and is not properly quoted to form a whole, the export is taken as the command to be run and the rest are saved in the positional parameters array. This means that export is run and foo=bar is passed to it as $0. The final result is the same as running

/bin/your_shell -c 'bash -c export'

The correct command would be:

ssh remote_host "bash -c 'export foo=bar'"
xhienne
  • 17,793
  • 2
  • 53
  • 69
9

ssh concatenates the arguments with spaces and has the login shell of the remote user interpret it, so in:

ssh localhost bash -c 'export foo=bar'

ssh is asking the remote shell to interpret the

bash -c export foo=bar

command (in effect, if the remote host is Unix-like, it will run the remote shell with the-shell, -c and bash -c export foo=bar as arguments).

Most shells will interpret that command line as running the bash command with bash, -c, export and foo=bar as arguments (so run export while $0 contains foo=bar) while you'd want it to run it with bash, -c and export foo=bar as arguments.

For that, you'd need to use a command line like:

ssh localhost "bash -c 'export foo=bar'"

(or:

ssh localhost bash -c \'export foo=bar\'

for that matters) so the:

bash -c 'export foo=bar'

command line be passed to the remote shell. That command line would be interpreted by most shells as running the bash command with bash, -c and export foo=bar as arguments. Note that using

ssh localhost 'bash -c "export foo=bar"'

would not work if the login shell of the remote user was rc or es for instance where " is not a special quoting operator. Single quotes are the most portable quoting operators (though there is some variation on how they are interpreted between shells, see How to execute an arbitrary simple command over ssh without knowing the login shell of the remote user? for more on that).