Transferring environment variable through SSH / quoting in bash/sh/csh/tcsh

I want to transfer an environment variable over SSH.

The "correct" way is using SendEnv/~/.ssh/environment, but that requires the server to support AcceptEnv or PermitUserEnvironment, which it does not in my case.

So instead I am thinking to set the variable on the remote site like this:

FOO=val
export FOO
ssh server export FOO=$FOO'; do_stuff_which_uses_FOO'

That part is easy. I want a generic solution, so no matter the content of $FOO it will work. E.g.

FOO="  '\""
export FOO
QFOO=`quote "$FOO"` # quote will return "\ \ \'\\\""
export QFOO
ssh server export FOO=$QFOO'; do_stuff_which_uses_FOO'

This works no matter if the sending or receiving shell is sh or bash.

However, I also need it to work for csh/tcsh. And I will not know in advance which shell the receiving end is running. That means I have to code something that will work both in /bin/sh and /bin/csh.

So far I have managed to get it working for sh/bash:

ssh server // followed by the below quoted
eval `echo $SHELL | grep -E "/(t)?csh" > /dev/null && echo setenv FOO \\\ \\\ \\\\\'\\\\\" || echo export FOO=\\\ \\\ \\\\\'\\\\\";` ; echo "$FOO"

I can also get it to work for csh/tcsh (the user csh has csh as login shell):

ssh csh@server // followed by the below quoted
eval `echo $SHELL | grep -E "/(t)?csh" > /dev/null && echo setenv FOO \\\ \\\ \\\\\'\\\\\" || echo export FOO=\\\ \\\ \\\\\'\\\\\";` ; echo "$FOO"

If $FOO is * or ? it works fine with BASH:

ssh server eval\ \`echo\ \$SHELL\ \|\ grep\ -E\ \"/\(t\)\?csh\"\ \>\ /dev/null\ \&\&\ echo\ setenv\ FOO\ \\\\\\\\\\\*\\\;\ \|\|\ echo\ export\ FOO=\\\\\\\\\\\*\\\;\`\;echo\ \"\$FOO\"\ a;

But it fails with csh:

ssh csh@server eval\ \`echo\ \$SHELL\ \|\ grep\ -E\ \"/\(t\)\?csh\"\ \>\ /dev/null\ \&\&\ echo\ setenv\ FOO\ \\\\\\\\\\\*\\\;\ \|\|\ echo\ export\ FOO=\\\\\\\\\\\*\\\;\`\;echo\ \"\$FOO\"\ a;
No match.
FOO: Undefined variable.

It seems * and ? refuse to be quoted with .

To answer this question your solution should:

  • be able to transfer an environment variable to the remote server
  • not use SendEnv/AcceptEnv/PermitUserEnvironment
  • work no matter if the source shell is sh/bash/csh/tcsh
  • work no matter if the destination shell is sh/bash/csh/tcsh
  • work no matter the content of the environment variable. Specifically it should at least work for: \n * space ' " ? < > ! $ \ and any combination of those.

If you can find a better way to transfer the variable than quoting it, then that is fine, too.


Solution 1:

I now have a working model for all characters except \n:

sub shell_quote_scalar {
    # Quote the string so shell will not expand any special chars
    # Returns:
    #   string quoted with \ as needed by the shell
    my $a = shift;
    $a =~ s/([\002-\011\013-\032\\\#\?\`\(\)\{\}\[\]\*\>\<\~\|\; \"\!\$\&\'\202-\377])/\\$1/g;
    $a =~ s/[\n]/'\n'/g; # filenames with '\n' is quoted using \'
    return $a;
}

sub env_quote {
    my $v = shift;
    $v =~ s/([ \n\&\<\>\(\)\;\'\{\}\t\"\$\`\*\174\!\?\~])/\\$1/g;
    return $v;
}

my @qcsh = map { my $a=$_; "setenv $a " . env_quote($ENV{$a})  } @vars;
my @qbash = map { my $a=$_; "export $a=" . env_quote($ENV{$a}) } @vars;

$Global::envvar =
    join"",
    (q{echo $SHELL | grep -E "/t?csh" > /dev/null && }
     . join(" && ", @qcsh)
     . q{ || }
     . join(" && ", @qbash)
     .q{;});

print shell_quote_scalar($Global::envvar);