9

My team is responsible for thousands of Linux/Unix machines, so naturally the root account is "shared" amongst the admins. I prefer vi mode, others prefer emacs mode.

How can I set bash's readline to vi mode upon SSH login to any machine, without forcing everybody else to use vi mode as well?

In essence would like to have the effect of set -o vi after login without actually having to type it every time, and without forcing it upon everybody else (as much as emacs mode is annoying to me, vi mode is annoying to them).

I know this wouldn't be a problem if everyone would use their own accounts with sudo to execute privileged commands, but due to circumstances outside of my control this sadly isn't an option.

  • 1
    That's not easy. One way is to parse sshd's log file and to see which key was used to log in. I was hoping for a client-side solution, for example a way to pass my local readline config on to the remote side or something like that, or some dark OpenSSH magic that silently executes set -o vi before giving me control of the shell. – Patrick Jan 27 '17 at 10:47
  • 1
    Maybe you could use an Expect script on the client that ssh's into the server, send the set -o vi command, then switches to interactive mode. – Barmar Jan 27 '17 at 20:56
  • 1
    At least the OpenSSH sshd sets up several environment variables which might help you determine who is on the other end. For example, SSH_CLIENT contains the connecting IP-address (and the outgoing/incoming port of the client as well). Fiddling with this in ~/.bashrc might allow you to do things just for you. – Sami Laine Jan 28 '17 at 11:53
  • 1
    The bounty says you're looking for a "client side solution" -- what is that? ssh on a Unix host? Putty? Hummingbird? Java SSH? Cygwin? – Jeff Schaller Feb 8 '17 at 17:52
  • 1
    You mention tens of thousands of machines. Do you want to start from one machine and get to these machines, or do you want to be able to hop from machine to machine to machine carrying vi mode with you? – icarus Feb 15 '17 at 3:28
3
+200

Here's a silly way to do it, which really only works well with public-key authentication:

First, ensure that your local machine has nc on it.

Second, still on your local machine, make a script (I'll call it connect-to-server) and put it in a place your ${PATH} knows about*:

#!/bin/sh
# connect-to-server
ssh -q server-hostname "touch .yourname" </dev/null >/dev/null 2>&1
nc server-hostname 22

Next, modify the .bashrc on the remote system to include somewhere:

# partial .bashrc
if [ -f "${HOME}/.yourname" ]; then
  rm "${HOME}/.yourname"
  set -o vi
fi

Finally, back in your local machine, edit ~/.ssh/config to add:

# partial ssh config
Host serverNickname
  Hostname server-hostname
  ProxyCommand connect-to-server

Downsides of this approach (and why I call it silly):

  • If there is an actual proxy command needed, it becomes more complicated.
  • If someone else logs in at the same time you do, there's a chance that the .yourname file will not yet have been deleted, in which case they get the set -o vi as well.
  • Most importantly, if you do ssh serverNickname command, then command will run, but (since .bashrc is never sourced) the .yourname file remains, so it would be polite to have a second alias in your ssh config that does not use the pseudo-proxy.

In fact, the only upside of this approach is that your ssh command does not need to be given any extra arguments.


*If you don't want to have to change anything on remote systems, here's an alternative pseudo-proxy that creates a temporary .bashrc:

#!/bin/sh
# connect-to-server
ssh -q server-hostname 'ln .bashrc .bashrc.real; cat .bashrc.real <(printf "set -o vi; ln -f .bashrc.real .bashrc\n") >.bashrc.yourname; ln -f .bashrc.yourname .bashrc' </dev/null >/dev/null 2>&1
nc server-hostname 22

This has all the same disadvantages of the other method, so you would still want a second alias in your ssh configuration that does not invoke the pseudo-proxy.

  • Very creative. Thank you for the suggestion. I use ProxyCommand extensively already (some networks / hosts require 5+ hops to reach) and this would quickly result in a configuration nightmare. Edit: Looking at all the answers I think yours comes closest. – Patrick Feb 10 '17 at 9:05
4

I'd go for:

ssh server -t "bash --login -o vi"

but it you are an admin, you can try something cleaner. For example you could use the ssh SendEnv option on the client side to transmit a specific variable, use AcceptEnv in the sshd configuration (server side) to accept it, and based on this, modify root's .bashrc file to adjust behaviour according to the value of the variable.

This implies to change the sshd configuration on all hosts as well as their .bashrc. Not exactly a "standalone" way to do, however...

3

For an easy client side solution:

alias connect='ssh -t root@server "bash -o vi"'

This will fail if the root's shell initialization scripts explicitly uses set -o emacs or sets EDITOR to emacs, or if root's .initrc file invokes emacs key bindings.

The rest of this answer concerns server-side solutions.


This works when you are ssh-ing into the machine and then use sudo -i:

For your /root/.bashrc:

if [[ -n "$SUDO_USER" ]] && [[ -f /root/.bashrc-"$SUDO_USER" ]]; then
  source /root/.bashrc-"$SUDO_USER"
fi

This allows you to have a personal bashrc file called /root/.bashrc-patrick wherein you can do whatever you like, like set -o vi.

Combining this with a somewhat naive approach to picking that rc file up depending on $SSH_CLIENT:

if [[ -n "$SUDO_USER" ]]; then
  person="$SUDO_USER"
elif [[ -n "$SSH_CLIENT" ]]; then

  case "$SSH_CLIENT" in
    192.168.216.100*)  person="joe" ;;
    192.168.216.120*)  person="patrick" ;;
    192.168.216.150*)  person="lindsey" ;;
  esac

fi

if [[ -n "$person" ]] && [[ -f /root/.bashrc-"$person" ]]; then
  source /root/.bashrc-"$person"
fi

This obviously only works if you're connecting from the same IP address all the time...

Another approach which uses the comment field of the particular SSH key you're using, which works if you're forwarding the SSH agent to the server:

ssh_comment="$( ssh-add -L | grep -f /root/.ssh/authorized_keys | awk '{ print $NF '} | head -n 1 )"

This picks out the comment field for the key that you used to connect to the server. The head -n 1 is there in case you happen to have several of your keys in the authorized_keys file.

You may then use $ssh_comment to pick an rc file to source, either directly as with the $SUDO_USER approach above (in which the comment in $ssh_comment may need to undergo some cleanup if it's a pathname), or via a case statement as with the $SSH_CLIENT approach.

  • Matching SSH_CLIENT and SUDO_USER is what I use currently, but it requires server side modification and it's not particularly reliable. I was hoping for a pure client-side solution. Thank you for the suggestion though. – Patrick Feb 10 '17 at 9:06
3

If you really want to do it without modification on the server side, either:

1) Run something like

$ ssh user@host -t 'bash -l -o vi' 

I don't think the documentation is too clear on that, but -o option is mentioned, and seems to work.

2) Use expect:

The expect script:

$ cat bashsetup.expect
#!/usr/bin/expect -f 

set user [lindex $argv 0];
set host [lindex $argv 1];

spawn ssh -l $user $host
expect "$ "
send "set -o vi\n"
interact

Make it executable and run:

$ ./bashsetup.expect user testhost
spawn ssh -l user testhost
[motd, blahblah...]
user@testhost ~$ set -o vi
user@testhost ~$ 

This assumes you can login without entering passwords (for the remote host or for your keys), otherwise the expect script would need to take that into account. But with lots of machines you're likely to already have that. Also, I expected for a dollar sign and a space, edit that according to your prompt: "# " perhaps.

Though if something printed before prompt includes those same characters, you'll need to include something more specific in the expected string.

Also, that script doesn't support giving extra arguments to ssh. If you're going to give an explicit command to run, you probably don't need vi-mode, but if you need say port tunneling, that might be a problem.


But in any case, I really think this should be solved on the target systems with separate accounts (sudo or just plain old UID 0). Personalised configuration would be useful for many other cases too, and in general you'd have a bunch of configuration files and environment variables you'd like to set. (Consider that the admins might not agree on the value of $EDITOR, or the contents of virc or whatever.)

Also removing users would be easier with separate accounts.

Any way of synchronising files on all the hosts would also trivially solve this by allowing you to login with something like ssh -t user@host 'patricks_shell.sh' or ssh -t user@host 'bash --rcfile patrick.rc'.

  • I thought of using expect, but as you already pointed out in your question the problem is matching something unique for the prompt to start interact. It doesn't exist, prompts may be way different, as are the motds / output from profiles. Thanks for the suggestion though. – Patrick Feb 10 '17 at 9:03
1

you can have a custom rc file following this guide:

User rc File | Secure Shell: The Definitive Guide

or inject a command line action in your authorized_keys file:

SSH config auto execute remote command | Unix & Linux Stack Exchange

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.