彷徨えるフジワラ

年がら年中さまよってます

bash の設定ファイル読み込み仕様

ログインシェルが bash に設定されている環境で、SSH 経由でログインなしのコマンド実行 (= 非対話的実行) を行うために、ユーザ独自の設定 (環境変数の変更/追加等) を設定ファイルに記述する際の注意点を、最初にまとめておきます。

  • SSH 経由でのコマンド実行に必要な追加設定は ~/.bashrc に記述する
  • ~/.bashrc への記述追加の際には:
    • [ -z "$PS1" ] && return のような、「非対話的実行時には、以降の設定評価を中断」する記述の有無を確認し
    • 記述がある場合は、それよりも前の行で追加設定を記述する

「非対話的実行」の判定は、上記以外の方法でも可能です。必ずしも上記と同一の記述で対処しているとは限りませんから、注意してください(本エントリ末尾でも、- パラメータを使った判定方法を例示しています)。

以下は、bash の設定ファイル読み込みの詳細に関する、備忘録代わりのまとめです。

なお、このエントリの記述内容には、Debian GNU/Linux の配布内容に依存する部分が多々あります: 例えば /etc/skel 配下の設定ファイル雛形の内容等。

実際に環境設定記述を行う際には、各自の運用環境の実情を、必ず確認するようにしてください。

設定ファイルの読み込み仕様の確認

bash のオンラインマニュアルによると、設定ファイルの読み込み挙動は、「ログイン (login) シェルか否か」と「対話的 (interactive) 実行か否か」によって、切り分けられています。

そこで、これらの組み合わせから、以下の4つのケースについて確認することにしましょう。

--- ログイン 対話的
A o o
B o ---
C --- o
D --- ---

ログインシェルである場合の、設定ファイル読み込みは、オンラインマニュアルによると:

When bash is invoked as an interactive login shell (A), or as a non-interactive shell with the --login option (B), it first reads and executes commands from the file /etc/profile (1), if that file exists. After reading that file, it looks for ~/.bash_profile (2-1), ~/.bash_login (2-2), and ~/.profile (2-3), in that order, and reads and executes commands from the first one that exists and is readable.

対話的 (= A) か否か (= B) に関わりなく、以下の順序で設定ファイルが読み込まれることになります。

  1. /etc/profile
    間接的に以下のファイルが読み込まれます
    • /etc/bash.bashrc
    • /etc/profile.d/*
  2. 以下の順で、先に見つかったファイルだけが読み込まれます
    1. ~/.bash_profile
    2. ~/.bash_login
    3. ~/.profile
      /etc/skel/.profile から複製された .profile の場合、間接的に ~/.bashrc を読み込みます

また、非ログインシェルが対話的に実行される場合 (= C) の、設定ファイル読み込みは:

When an interactive shell that is not a login shell is started (C), bash reads and executes commands from /etc/bash.bashrc (1) and ~/.bashrc (2), if these files exist.

以下の順序で設定ファイルが読み込まれることになります。

  1. /etc/bash.bashrc
  2. ~/.bashrc

さて、「非ログインシェルの非対話的実行」 (= D) の場合の設定ファイル読み込みに関しては、実はオンラインマニュアル上での明示的な言及がないのですが、私が実験した限りでは、このようなケースでは、設定ファイル類は一切読み込まれません: 「BASH_ENV 環境変数が定義されている場合は云々〜〜」の記述が、「それ以外のケースでは、設定ファイルを読み込まないよ」ということを、暗に示しているのかもしれません。

例えばログインシェル上からの bash -c "コマンド 引数 ..." 実行などが、このケースに該当します。

ところが、SSH 経由でのログイン無しのコマンド実行の場合は、「非ログインシェルの非対話的実行」 (= D) にも関わらず、「非ログインシェルの対話的実行」(= C) と同じ、以下のような挙動になります。

  • ~/.profile は読み込まれない
  • ~/.bashrc は読み込まれる

これは、リモートコマンド実行時に関する、以下の特例が有効になるためです。

Bash attempts to determine when it is being run with its standard input connected to a a network connection, as if by the remote shell daemon, usually rshd, or the secure shell daemon sshd. If bash determines it is being run in this fashion, it reads and executes commands from ~/.bashrc (1) and ~/.bashrc (2), if these files exist and are readable.

1つ目の ~/.bashrc は、おそらく /etc/bash.bashrc の事でしょう。

非対話的実行時の .bashrc 記述の留意点

先述したように、SSH 経由でのアクセスの際には、対話的実行の有無に関わらず ~/.bashrc が読み込まれます。つまり、独自の設定変更記述等を ~/.bashrc に追記しておけば、その記述は SSH 利用時は常に有効になる筈なのです。

しかし、~/.bashrc に独自設定を記述しても、非対話的実行では機能しない場合があります

この原因は、デフォルト (= /etc/skel/.bashrc からの複製) の ~/.bashrc の冒頭にある、以下のような記述にあります:

# If not running interactively, don't do anything
[ -z "$PS1" ] && return

環境変数 PS1 は、プロンプト表示を設定する環境変数で、対話的実行の場合にのみ、~/.bashrc 読み込みの前の段階で設定されます。

つまり、PS1 環境変数が設定されていない = 非対話的実行の場合は、この行以降の設定記述は評価されません。

~/.bashrc に独自の設定を記述する場合、ファイル末尾に追加するのが一般的でしょう。その結果、対話的実行時のみしか追加設定が評価されないことになるわけです。

私はこれに気付かなかったせいで、「SSH での非対話的実行は、~/.bashrc 等の設定ファイル読み込み自体が抑止される」と誤解してしまっていました…… orz

閑話休題

結論として、~/.bashrc に追記した独自設定を、非対話的実行でも有効にするには、[ -z "$PS1" ] && return 記述よりも、前の行で記述しなければならない、ということになります。

本来対話的実行時しか実行されない /etc/bash.bashrc や ~/.bashrc が、非対話的実行となるリモートコマンド実行時にも読み込まれることを考えると、これらのファイルに「非対話的実行時には、以降の設定評価を中断」するための処理がわざわざ記述されているのも合点がいきますね。

bash 設定ファイル記述のテクニック

ログインシェルか否かの判定には、shopt 組み込みコマンドを使用します。

if shopt -q login_shell; then
    # ログインシェルの場合のみ実行
else
    # 非ログインシェルの場合のみ実行
fi

また、対話的実行か否かは、- パラメータに文字 i が含まれているか否かで判定します。

case "$-" in
*i*)
    # 対話的実行の場合のみ実行
    ;;
*)
    # 非対話的実行の場合のみ実行
    ;;
esac

対話的実行の有無は、先述した PS1 環境変数設定の有無で判定しても良いのですが、ユーザの追加設定記述によって、PS1 が不正に定義されている可能性もありますので、- パラメータを使う方が確実でしょう。

なお、環境変数設定を、以下のような単純な追加形式で記述すると:

PATH=${PATH}:${HOME}/bin

例えば「ログイン (= 対話的ログインシェル実行) → emacs 起動 → シェルバッファ実行 (= 対話的非ログインシェル実行)」のような、シェルの実行が入れ子になった際に、シェル起動の数だけ設定が追加されてしまいます。

このような状況を回避するには、シェル実行の入れ子レベルを保持する SHLVL を使用します。

if test "${SHLVL}"x = "1"x; then
    # 初回の bash 起動
    PATH=${PATH}:${HOME}/bin
else
    # 入れ子の bash 起動
fi