What happens on the remote side
Local ssh user@server 'shell_code'
makes the SSH server run something like:
"$SHELL" -c 'shell_code'
I'm not claiming it's exactly like this, but it's certainly close enough to understand what happens. I wrote it as if it was invoked in a shell, so it looks familiar; but in fact there is no shell yet.
A shell invoked this way exits when it reaches the end of the shell_code
. In your case the code is multi-line, this changes nothing.
Without shell code, e.g. upon plain ssh user@server
, the SSH server starts something different:
"$SHELL" -i
Again, I'm not claiming it's exactly like this.
A shell invoked this way is an interactive shell that prints a prompt and waits for input. It seems this is the behavior you wanted to achieve.
It's the SSH server that deliberately uses one form or the other, depending on the form of local ssh
invocation. The latter form works well for interactive use and the former form equally well runs some non-interactive shell code remotely, as if it was local. E.g. locally you can do:
<data md5sum
but if you are missing md5sum
locally, you can use a remote one:
<data ssh user@server md5sum
Like local md5sum
exits after doing its job, ssh … md5sum
also exits after the remote md5sum
does its job. Quite useful.
Or you can pipe to a remote file:
<data ssh user@server 'cat >copy'
and cat
, the remote shell and the local ssh
will exit automatically when appropriate.
Or you can ask server the time:
ssh user@server date
and it will behave like local date
, it won't put you in an interactive remote shell.
When interactive shells exit automatically
I expected it [ssh … date
] to not exit by default. When you type commands in your terminal, does it exit by default?
It does, really. An interactive shell does exit when it reaches EOF when reading commands. The thing is it doesn't "experience" EOF from the terminal until you hit Ctrl+d (once or twice). This is how it works with a terminal; but if you make an interactive shell read from a regular file or from a pipe then it will see EOF exactly when you expect this:
{ echo date; echo sleep 5; echo date; } | bash -i
and it will exit.
So every shell exits by default when there is truly nothing more to do. Interactive shells allegedly "don't exit" because reading from a terminal is deliberately designed not to trigger EOF when there is no input at the moment, with an assumption that input may come any minute. It's about the terminal, not about the shell nor any program in general; I mean e.g. </etc/fstab cat
exits but plain cat
reading from a tty can work indefinitely, it's the same story.
Importance of remote tty
The two ways an SSH server can run a shell differ not only in their forms. Remote interactive commands work well when they use a tty they find local (so it's a tty on the server side, it sits between local ssh
and the commands); but remote non-interactive commands (like our example md5sum
) work well when there is no tty allocated. The SSH server allocates a tty or not and it does it automatically (but there are options for ssh
to explicitly choose if needed: -t
, -tt
, -T
). I think you can get some insight from the following question (and answer) of mine, where I tried to get the best from both at once: ssh with separate stdin, stdout, stderr AND tty.
What you can do
It looks you want to get a remote interactive shell that executes some shell code before giving you the first prompt. The easiest way is to put the code into the right startup script (~/.bashrc
if the shell is Bash) and just ssh
(without passing any shell code) to an interactive remote shell. It's a permanent solution, not useful if you want to be able to run one-time code on demand and still get a prompt.
To execute shell code on demand you need a contraption like this:
ssh -t … '
exec bash --rcfile <(cat <<"EOF"
# your custom shell code here
. ~/.bashrc
# and/or here
EOF
) -i'
This assumes the shell to be started by the SSH server understands <(…)
(Bash does understand) and the shell you want as the ultimate interactive shell is Bash.
The SSH server will run a non-interactive shell, still with a tty (because of -t
). This shell will exec
to an interactive Bash that will source a custom file instead of ~/.bashrc
. The file will be a pipe from the process substitution and its content will be your custom shell code plus the instruction to source ~/.bashrc
anyway. This way you will make the ultimate shell execute your custom shell code and then give you the prompt.
Note an interactive shell spawned directly by the SSH server would be a login shell, it would source /etc/profile
. Here neither shell is a login shell. We could run the ultimate shell with -l
, but it would ignore --rcfile
then.
However, your specific custom shell code sets up things that survive exec
(they can be inherited in general). This means we can simplify our snippet to:
ssh -t … '
cd codes/vbe
eval $(ssh-agent)
ssh-add -D
ssh-add ~/.ssh/id_vbe
exec bash -i
'
And I think in this case you can add -l
and make the ultimate Bash a login shell, if this is what you need. Remember that login and non-login shells source different files; the files may source one another (e.g. ~/.bash_profile
may source ~/.bashrc
); even non-interactive Bash may source ~/.bashrc
; and there are two remote shells in sequence, each may source something. It's likely the remote ~/.bashrc
will be sourced two times. Depending on its content side effects may appear.
A very different alternative is to invoke ssh …
(without shell code) from a local expect
script. The script should inject the desired shell code and finally let you interact
. The below quick and dirty example assumes that your remote prompt can be detected by matching @*:*$
.
expect -c '
spawn ssh -i alex-kp.pem ec2-user@ec2-xxx.us-west-2.compute.amazonaws.com
expect "@*:*$ "
send "cd codes/vbe\r"
expect "@*:*$ "
send "eval \$(ssh-agent)\r"
expect "@*:*$ "
send "ssh-add -D\r"
expect "@*:*$ "
send "ssh-add ~/.ssh/id_vbe\r"
interact
'
I don't know AWS, but in general ssh-agent
started by eval $(ssh-agent)
may or may not survive after you disconnect. If it's going to survive then manual cleaning (ssh-agent -k
) before exiting the remote shell is a good idea.
ssh … date
not to exit? Afterdate
the remote non-interactive shell has nothing more to do and thus it exits. – Kamil Maciorowski Jan 09 '24 at 21:54