ssh上でローカル.bashrcを使用し、コマンド履歴を統合する

sshを介して多数のリモートマシンを操作する必要がある場合、これらのマシンのシェル環境をどのように統合するかという疑問が生じます。事前に.bashrcをコピーすることはあまり便利ではなく、多くの場合不可能です。接続中に直接コピーすることを検討しましょう。



[ -z "$PS1" ] && return

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

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


これは非常に素朴な方法であり、いくつかの明らかな欠点があります。



  • 既存の.bashrcを上書きできます
  • 1つの接続の代わりに、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
...


現在、多重化によって1つの接続のみを使用しています。.bashrcは、デフォルトでbashによって使用されないファイルにコピーされ、-rcfileオプションを使用して明示的に指定します。function引数は、リモートマシンのアドレスだけでなく、他の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変数をオーバーライドします。これ以降、新しいインタラクティブシェルを開始すると、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を使用した場合



  • asconfig-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を実行して、その出力をファイルにリダイレクトします。



update_eternal_history関数は、bashプロンプトが表示される直前に呼び出されます。この関数は、最後のコマンドが重複しているかどうかを確認し、重複していない場合は、転送されたポートに送信します。ポートが構成されていない場合(ローカルマシンの場合)、または送信中にエラーが発生した場合、保存はローカルファイルに送られます。



sshb関数は、ポート転送を設定し、update_eternal_historyがサーバーにデータを送信するために使用するsymlinkを作成することで補完されました。



このソリューションには欠点があります。



  • netcatのポートはハードコードされており、競合が発生する可能性があります
  • ( - - ), , ,


私自身の.bashrcはここで見ることができます



提案されたソリューションを改善する方法についてアイデアがある場合は、コメントで共有してください。



更新。ubuntu 16.04で、問題が発生しました。netcatが複数の接続でフリーズし、100%cpuを消費します。私はsocatに切り替えました、予備テストはすべてがうまくいくことを示しました。また、履歴が送信されるアドレスを決定するsymlinkを管理するためのロジックが追加されました。それは次のようになりました:



#!/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