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 variableLC_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 ofprintf
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:
- Copy your
.bashrc
to a temporary remote file. You need to do this only once until the temporary file is deleted. - 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.)