在ssh上使用本地.bashrc并合并命令历史记录

如果必须通过ssh使用大量远程计算机,那么问题就出在如何统一这些计算机上的shell环境上。预先复制.bashrc并不是很方便,而且通常是不可能的。让我们考虑在连接期间直接复制:



[ -z "$PS1" ] && return

sshb() {
    scp ~/.bashrc ${1}:
    ssh $1
}

# the rest of the .bashrc
alias c=cat
...


这是一种非常幼稚的方法,具有几个明显的缺点:



  • 您可以覆盖现有的.bashrc
  • 建立一个2,而不是一个连接
  • 因此,您还必须登录2次。
  • 函数参数只能是远程计算机的地址


改进的选项:



[ -z "$PS1" ] && return

sshb() {
    local ssh="ssh -S ~/.ssh/control-socket-$(tr -cd '[:alnum:]' < /dev/urandom|head -c8)"
    $ssh -fNM "$@"
    $ssh placeholder "cat >~/.bash-ssh" <~/.bashrc
    $ssh "$@" -t "bash --rcfile ~/.bash-ssh -i"
    $ssh placeholder -O exit >/dev/null 2>&1
}

# the rest of the .bashrc
alias c=cat
...


现在,我们仅通过多路复用使用一个连接。默认情况下,.bashrc复制到bash不使用的文件,我们通过--rcfile选项显式指定它。函数参数不仅可以是远程计算机的地址,还可以是其他ssh选项。



原则上,可以停止这样做,但是最终的解决方案具有令人不快的缺点。如果运行screen或tmux,则将使用远程计算机上的.bashrc,并且所有别名和功能都将丢失。幸运的是,这可以克服。为此,我们需要创建一个包装器脚本,将其声明为新的外壳程序。为简单起见,我们假设远程机器上已经有一个包装器脚本,并且该脚本位于〜/ bin / bash-ssh中。该脚本如下所示:



#!/bin/bash
exec /bin/bash --rcfile ~/.bash-ssh “$@


和.bashrc像这样:



[ -n "$SSH_TTY" ] && export SHELL="$HOME/bin/bash-ssh"

[ -z "$PS1" ] && return

sshb() {
    local ssh="ssh -S ~/.ssh/control-socket-$(tr -cd '[:alnum:]' < /dev/urandom|head -c8)"
    $ssh -fNM "$@"
    $ssh placeholder "cat >~/.bash-ssh" <~/.bashrc
    $ssh "$@" -t "bash --rcfile ~/.bash-ssh -i"
    $ssh placeholder -O exit >/dev/null 2>&1
}

# the rest of the .bashrc
alias c=cat
...


如果存在SSH_TTY变量,则说明我们在远程计算机上,并覆盖SHELL变量。从这一刻起,当启动一个新的交互式shell时,将启动一个脚本,该脚本将使用建立ssh会话时保存的非标准配置启动bash。



为了获得方便的工作解决方案,仍然需要弄清楚如何在远程计算机上创建包装器脚本。原则上,您可以在我们保存的bash配置中创建它,如下所示:



[ -n "$SSH_TTY" ] && {
    mkdir -p "$HOME/bin"
    export SHELL="$HOME/bin/bash-ssh"
    echo -e '#!/bin/bash\nexec /bin/bash --rcfile ~/.bash-ssh "$@"' >$SHELL
    chmod +x $SHELL
}


但是实际上您可以通过一个〜/ .bash-ssh文件来解决:



#!/bin/bash

[ -n "$SSH_TTY" ] && [ "${BASH_SOURCE[0]}" == "${0}" ] && exec bash --rcfile "$SHELL" "$@"

[ -z "$PS1" ] && return

sshb() {
    local ssh="ssh -S ~/.ssh/control-socket-$(tr -cd '[:alnum:]' < /dev/urandom|head -c8)"
    $ssh -fNM "$@"
    $ssh placeholder "cat >~/.bash-ssh" <~/.bashrc
    $ssh "$@" -t 'SHELL=~/.bash-ssh; chmod +x $SHELL; bash --rcfile $SHELL -i'
    $ssh placeholder -O exit >/dev/null 2>&1
}


# the rest of the .bashrc
alias c=cat
...


现在〜/ .bash-ssh文件既是独立脚本又是bash配置。它是这样的。在本地计算机上,[-n“ $ SSH_TTY”]之后的命令将被忽略。在远程计算机上,sshb函数创建一个〜/ .bash-ssh文件,并将其用作启动交互式会话的配置。构造[“ $ {BASH_SOURCE [0]}” ==“ $ {0}”]使您可以确定文件是由另一个脚本上载还是作为独立脚本启动。结果,当使用〜/ .bash-ssh时



  • 作为配置-exec被忽略
  • 就像脚本一样-控件传递到bash,〜/ .bash-ssh的执行以exec结尾。


现在,通过ssh连接时,您的环境到处都会看起来一样。用这种方法工作要方便得多,但是命令执行的历史记录将保留在您连接的计算机上。就个人而言,我想将历史记录保存在本地,以便我可以准确回顾过去在某些计算机上所做的事情。为此,我们需要以下组件:



  • 本地计算机上的TCP服务器,它将从套接字接收数据并将其重定向到文件
  • 将此服务器的侦听端口转发到我们通过ssh连接的计算机
  • bash设置中的PROMPT_COMMAND,将在命令完成后将历史记录更新发送到转发的端口


可以这样完成:



#!/bin/bash

[ -n "$SSH_TTY" ] && [ "${BASH_SOURCE[0]}" == "${0}" ] && exec bash --rcfile "$SHELL" "$@"

[ -z "$PS1" ] && return

[ -z "$SSH_TTY" ] && {
    history_port=26574
    netstat -lnt|grep -q ":${history_port}\b" || {
        umask 077 && nc -kl 127.0.0.1 "$history_port" >>~/.bash_eternal_history &
    }
}

HISTSIZE=$((1024 * 1024))
HISTFILESIZE=$HISTSIZE
HISTTIMEFORMAT='%t%F %T%t'

update_eternal_history() {
    local histfile_size=$(stat -c %s $HISTFILE)
    history -a
    ((histfile_size == $(stat -c %s $HISTFILE))) && return
    local history_line="${USER}\t${HOSTNAME}\t${PWD}\t$(history 1)"
    local history_sink=$(readlink ~/.bash-ssh.history 2>/dev/null)
    [ -n "$history_sink" ] && echo -e "$history_line" >"$history_sink" 2>/dev/null && return
    local old_umask=$(umask)
    umask 077
    echo -e "$history_line" >> ~/.bash_eternal_history
    umask $old_umask
}

[[ "$PROMPT_COMMAND" == *update_eternal_history* ]] || export PROMPT_COMMAND="update_eternal_history;$PROMPT_COMMAND"

sshb() {
    local ssh="ssh -S ~/.ssh/control-socket-$(tr -cd '[:alnum:]' < /dev/urandom|head -c8)"
    $ssh -fNM "$@"
    local bashrc=~/.bashrc
    [ -r ~/.bash-ssh ] && bashrc=~/.bash-ssh && history_port=$(basename $(readlink ~/.bash-ssh.history))
    local history_remote_port="$($ssh -O forward -R 0:127.0.0.1:$history_port placeholder)"
    $ssh placeholder "cat >~/.bash-ssh; ln -nsf /dev/tcp/127.0.0.1/$history_remote_port ~/.bash-ssh.history" < $bashrc
    $ssh "$@" -t 'SHELL=~/.bash-ssh; chmod +x $SHELL; bash --rcfile $SHELL -i'
    $ssh placeholder -O exit >/dev/null 2>&1
}

# the rest of the .bashrc
alias c=cat
...


[-z“ $ SSH_TTY”]之后的块仅在本地计算机上有效。我们检查端口是否繁忙,如果没有,请在其上运行netcat,将其输出重定向到文件。



在显示bash提示之前,将调用update_eternal_history函数。此函数检查最后一个命令是否重复,如果不是,则将其发送到转发的端口。如果未配置端口(对于本地计算机),或者在发送时发生错误,则保存将保存到本地文件。



通过设置端口转发并创建符号链接来补充sshb函数,update_eternal_history将使用该符号链接将数据发送到服务器。



此解决方案并非没有缺点:



  • netcat的端口是硬编码的,有可能发生冲突
  • ( - - ), , ,


我自己的.bashrc可以在这里查看



如果您有关于如何改进建议的解决方案的想法,请在评论中分享。



更新。在ubuntu 16.04上,我遇到了一个问题:netcat在多个连接上冻结,并占用了100%的cpu。我切换到socat,初步测试表明一切都很好。还添加了用于管理符号链接的逻辑,该逻辑确定了将历史记录发送到的地址。原来是这样的:



#!/bin/bash

[ -n "$SSH_TTY" ] && [ "${BASH_SOURCE[0]}" == "${0}" ] && exec bash --rcfile "$SHELL" "$@"

[ -z "$PS1" ] && return

[ -z "$SSH_TTY" ] && command -v socat >/dev/null && {
    history_port=26574
    netstat -lnt|grep -q ":${history_port}\b" || {
        umask 077 && socat -u TCP4-LISTEN:$history_port,bind=127.0.0.1,reuseaddr,fork OPEN:$HOME/.bash_eternal_history,creat,append &
    }
}

HISTSIZE=$((1024 * 1024))
HISTFILESIZE=$HISTSIZE
HISTTIMEFORMAT='%t%F %T%t'

update_eternal_history() {
    local histfile_size=$(stat -c %s $HISTFILE)
    history -a
    ((histfile_size == $(stat -c %s $HISTFILE))) && return
    local history_line="${USER}\t${HOSTNAME}\t${PWD}\t$(history 1)"
    local history_sink=$(readlink ~/.bash-ssh.history 2>/dev/null)
    [ -n "$history_sink" ] && echo -e "$history_line" >"$history_sink" 2>/dev/null && return
    local old_umask=$(umask)
    umask 077
    echo -e "$history_line" >> ~/.bash_eternal_history
    umask $old_umask
}

[[ "$PROMPT_COMMAND" == *update_eternal_history* ]] || PROMPT_COMMAND="update_eternal_history;$PROMPT_COMMAND"

sshb() {
    local ssh="ssh -S ~/.ssh/control-socket-$(tr -cd '[:alnum:]' < /dev/urandom|head -c8)"
    local bashrc=~/.bashrc
    local history_command="rm -f ~/.bash-ssh.history"
    [ -r ~/.bash-ssh ] && bashrc=~/.bash-ssh && history_port=$(basename $(readlink ~/.bash-ssh.history 2>/dev/null))
    $ssh -fNM "$@"
    [ -n "$history_port" ] && {
        local history_remote_port="$($ssh -O forward -R 0:127.0.0.1:$history_port placeholder)"
        history_command="ln -nsf /dev/tcp/127.0.0.1/$history_remote_port ~/.bash-ssh.history"
    }
    $ssh placeholder "${history_command}; cat >~/.bash-ssh" < $bashrc
    $ssh "$@" -t 'SHELL=~/.bash-ssh; chmod +x $SHELL; bash --rcfile $SHELL -i'
    $ssh placeholder -O exit >/dev/null 2>&1
}

# the rest of the .bashrc
alias c=cat
...



All Articles