Execute bash commands over SSH while staying in interactive mode afterwards

In other words, you'd like to have a remote ~/.bashrc, but you can't, not even under another name. Bash doesn't support passing initial commands as command line arguments or via environment variables, they have to be in a file. However, the file doesn't have to be on the filesystem!

bash --rcfile /dev/fd/3 3< <(echo 'alias foo="echo hello"')
$ foo
hello

Many SSH servers allow transmitting environment variables whose name is of the form LC_XXX, because they are usually used to indicate locales, which need to be transmitted between hosts and have no security implication. If yours allows that, you can transmit the content of your .bashrc via an environment variable, then feed that environment variable into a file descriptor.

LC_BASHRC=$(cat ~/.bashrc; exec 3<&-) \
ssh -t remote.example.com \
    'exec bash --rcfile /dev/fd/3 3< <(printf %s "$LC_BASHRC")'
  • The content of the local ~/.bashrc is transmitted to the server in the environment variable LC_BASHRC.
  • exec 3<&- is appended, to close the file descriptor after reading the file.
  • On the remote side, the login shell is replaced (exec) by a new instance of bash, which is told to read its initialization file on file descriptor 3.
  • 3< <(…) redirects file descriptor 3 to a command substitution: the output of printf is fed to the parent process via a pipe.

If your login shell on the server is /bin/sh rather than bash or ksh, you can't use the process substitution directly in that shell, you need an extra layer:

LC_BASHRC=$(cat ~/.bashrc; exec 3<&-) \
ssh -t remote.example.com \
    'exec bash -c '\''exec bash --rcfile /dev/fd/3 \
                           3< <(printf %s "$LC_BASHRC")'\'

You can check which environment variables the SSH server accepts by looking for the AcceptEnv directive in its [configuration](http://www.openbsd.org/cgi-bin/man.cgi?query=sshd_config&sektion=5) (/etc/sshd_configor/etc/ssh/sshd_config`).

Instead of using command substitution 3< <(…), you can use a here-string. This creates a temporary file (in $TMPDIR or /tmp) rather than a pipe, so this only works if you don't mind creating a temporary file.

LC_BASHRC=$(cat ~/.bashrc; exec 3<&-) \
ssh -t remote.example.com \
    'exec bash --rcfile /dev/fd/3 3<<<"$LC_BASHRC"'

If you don't mind creating a temporary file, there is a much simpler technique:

  1. Copy your .bashrc to a temporary remote file. You need to do this only once until the temporary file is deleted.
  2. Launch an interactive shell with --rcfile pointing to the temporary file.
remote_bashrc=$(ssh remote.example.com 'bashrc=$(mktemp) && cat >>"$bashrc" && echo "$bashrc"' <~/.bashrc)
ssh -t remote.example.com "exec bash --rcfile '$remote_bashrc'"

If the SSH implementation isn't too antique, you can use a master connection to speed up the launch of multiple SSH commands to the same host.

If you're logging in with a key and you have control over the public key file, you can automate more. In ~/.ssh/authorized_keys, a key can have a command=… directive that lets you run a command instead of what was specified on the command line. See How can I set environment variables for a remote rsync process? for more explanations.

command="if [ -n \"$SSH_ORIGINAL_COMMAND\" ]; then
           eval \"$SSH_ORIGINAL_COMMAND\";
         else exec bash -c 'bash 3<<<\"$LC_BASHRC\"'; fi" ssh-rsa …

or

command="if [ -n \"$SSH_ORIGINAL_COMMAND\" ]; then
           eval \"$SSH_ORIGINAL_COMMAND\";
         else exec bash -c 'bash 3<<<\"alias foo=bar; …\"'; fi" ssh-rsa …

(This needs to be all on one line, I only put line breaks for legibility.)

Tags:

Bash

Ssh