bash の初期化ファイル .profile, .bashrc, .bash_profile の使い分けと管理方針

f:id:Naotsugu:20191113235117p:plain


はじめに

本記事では bash の初期化ファイルとそれにまつわる運用方法について説明します。


シェル(bash)の初期化ファイルには .profile .bashrc, .bash_profile などがありますが、どこに何を定義すべきかについては色々な意見が散見され、結果ぼんやりしてしまうことがあります。


色々な意見が散見されるのは、唯一の正解などといったものは無いからなのですが、ここでは改めてシェルの初期化ファイルについて整理し、管理方針についての判断材料を提供したいと思います。


bash の初期化ファイル

ディストリビューションなどによりインポートするファイルが異なったり、オプションにより読み込むファイルを変更できたりしますが、bash の起動時には以下のような判定で読み込む初期化ファイルが変化します。


f:id:Naotsugu:20191110015450p:plain

青塗りの四角が読み込む設定ファイルを表しています。

大きくは、ログインシェルかそうではないか、インタラクティブかそうではないかで読み込むファイルが別れます。


ログインシェルの場合、/etc/profile というシステムワイドの設定ファイルを読み、続いてユーザ設定ファイル ~/.bash_profile, ~/.bash_login, ~/.profile の順番で最初に見つかったものだけを読みます。

よくあるのが、~/.profile を使っていたけど、~/.bash_profile を作成したら設定が読み込まれなくなった などは、このようなファイルの検索順序が影響するためです。


ログインシェルではない場合は ~/.bashrc ファイルを読みます。 非インタラクティブの場合は $BASH_ENV に指定されたファイルを初期化ファイルとして読み込みます。


図では省略していますが、ログインシェルの場合でも非インタラクティブの場合は、環境変数 $BASH_ENV に設定されたファイルを初期化ファイルとして読みます。

$BASH_ENV を読むケースは、主にシェルスクリプトを実行する場合に該当します。


ケースに応じて読み込むファイルが変わることがわかったところで、分岐の判定にある、ログインシェル と インタラクティブシェル とは何かについて見ていきましょう。


ログインシェルとは

ログインシェルとはデスクトップ画面や仮想コンソールでログインしたときに起動されるシェルを指します。

bash の man page から抜粋すると以下のように説明されています。


linuxjm.osdn.jp

A login shell is one whose first character of argument zero is a -, or one started with the --login option.

ログインシェル(login shell)とは、0 番目の引き数の最初の文字が - であるシェル、または --login オプション付きで起動されたシェルのことです。


ここでの 0 番目の引数とは、bash に渡されたコマンドライン引数の最初の要素を意味します。 通常この引数にはプログラム起動時のコマンド名が入ります。

-bash というコマンド名で起動した場合、最初の文字が - なのでログインシェルとして起動することになります。


Linux におけるログインコマンドのソースファイル(login.c)は以下のようになっています。

tbuf[0] = '-';
xstrncpy(tbuf + 1, ((p = strrchr(pwd->pw_shell, '/')) ?
            p + 1 : pwd->pw_shell), sizeof(tbuf) - 1); // "-bash"

childArgv[childArgc++] = pwd->pw_shell; // "/bin/bash"
childArgv[childArgc++] = xstrdup(tbuf); // "-bash"
childArgv[childArgc++] = NULL;

execvp(childArgv[0], childArgv + 1);


上記ソースの2行目にある pwdpasswd 構造体で、/etc/passwd のエントリを参照しています。

この passwd ファイルは以下のような構成になっています。

root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
...

1行に、ユーザ名:パスワード:ユーザID:グループID:コメント欄:ホームディレクトリ:ログインシェル が定義され、このエントリからログインシェルとして使うシェルを取得しています。


login.c のソースに戻り、最終行 execvp() の第一引数 childArgv[0] には /bin/bash のようなコマンドのパス、第二引数の配列の最初の要素にはコマンド名として -bash のように指定されることが分かります。

これにより先頭文字が - となり、bash がログインシェルとして起動することになります。


具体的には以下のようなケースでログインシェルが起動します。

  • ログインプロンプトよりシステムにログイン
  • bash --login のように --login オプションを明示して bash を起動
  • su - <user> のように -(-lまたは--login)オプションを指定した場合、su コマンドがシェル名の前に - を付加
  • ssh user@hostname などでSSHログイン


システムへログイン、再ログインするような、ユーザセッションの開始時に起動するシェルがログインシェルになります。


インタラクティブ(対話的)シェルとは

bash の man page から抜粋すると以下のように説明されています。

An interactive shell is one started without non-option arguments and without the -c option whose standard input and error are both con-nected to terminals (as determined by isatty(3)), or one started with the -i option. PS1 is set and $- includes i if bash is interactive, allowing a shell script or a startup file to test this state.

対話的なシェルとは、 オプションでない引き数がなく、 標準入力と標準エラー出力がいずれも端末に接続されていて (これは isatty(3) で調べられます)、 -c オプションが指定されていない状態で起動されたシェル、または -i オプション付きで起動されたシェルのことです。 bash が対話的に動作している場合には、 PS1 が設定され、 $- に i が含まれます。 これを利用すると、対話的動作の状態であるかどうかを、 シェルスクリプトや起動ファイルの内部で調べられます。


ざっくりまとめると、以下の状態で起動したシェルがインタラクティブシェル(対話的)となります。

  • 標準入力と標準エラー出力が端末に接続されている
  • -c オプションが指定されていない
  • オプション以外の引数が指定されていない

または


出力は端末画面に表示されて、対話的に入力を受け付け、シェルスクリプトを実行するために起動されたシェルではない 場合にインタラクティブシェルとなります。

具体的には以下のケースでインタラクティブシェルになります。

  • 仮想コンソールや ssh によるログイン
  • su [user] のように - 無しで substitute user した場合
  • bash としてシェルを起動
  • bash -iシェルスクリプトを実行


インタラクティブ(対話的)ではない場合には、非インタラクティブ(非対話的)シェルとなります。

bash の man page では、非インタラクティブ(非対話的) な場合の起動は以下のように説明されています。

(例えばシェルスクリプトを実行するために) 非対話的に起動されると、 bash環境変数 BASH_ENV を調べ、この変数が定義されていればその値を展開し、 得られた値をファイル名とみなして、 そこからコマンドの読み込みと実行を行います。 つまり bash は以下のコマンドが実行されたのと同じように動作します:

if [ -n "$BASH_ENV" ]; then . "$BASH_ENV"; fi


シェルスクリプトの実行や、bash -c <command> のようにコマンドを実行した場合は インタラクティブ となります。 デスクトップ画面にログインしたときや、シェルスクリプトを通常実行した際に起動されたシェルは非インタラクティブです。


bash の man にはある通り、対話的な場合は $-i が含まれ、非対話的な場合は i が含まれません。

いくつか例を見てみましょう

$ echo $-
himBH      # インタラクティブ

$ bash -c 'echo $-'
hBc        # 非インタラクティブ

$ echo -e '#!/bin/sh\necho $-' > test.sh && chmod 777 test.sh
$ ./test.sh
hB        # 非インタラクティブ

なお、$- はシェル起動時に指定されたフラグの一覧を表す特殊変数です。

このように、インタラクティブかそうでは無いかを分別するのは、シェルの2面性が影響します。つまりコマンドインタプリタとしてユーザインターフェースを担う側面と、スクリプト言語としてプログラムの実行を担う側面です。

2面性が別の機能として提供されていれば、インタラクティブかそうではないかなどを場合分ける必要もなかったはずですが、シェルがこの2面性を抱えて機能拡張されてきた歴史上、このようになっています。


さて、インタラクティブ/非インタラクティブについては一つ注意点があります。

以下の man page にあるように、ネットワーク経由の場合はインタラクティブ扱いになります。

bash は、リモートシェルデーモン rshd やセキュアシェルデーモン sshd によって実行された場合など、標準入力がネットワーク接続に接続された 状態で実行されたかどうかを調べます。 この方法によって実行されていると bash が判断した場合、 ~/.bashrc が存在し、かつ読み込み可能であれば、 bash はコマンドをこのファイルから読み込んで実行します。


ssh localhost 'ls -l' のようにリモートでコマンドを実行した場合は、ログインシェルでもなく、コマンドを直接指定しているので非インタラクティブなはずですが、ネットワークにつながっているとしてインタラクティブ扱いされて ~/.bashrc が読まれます。


ログインシェルとインタラクティブシェルの分類

ここまで見てきたログインシェルとインタラクティブシェルはそれぞれ直行しており、ログインシェルでありインタラクティブシェルであったり、非ログインシェルであり、非インタラクティブシェルであったり組み合わせがあります。

ログインシェルとインタラクティブシェルの組み合わせを具体例にすると以下のようになります。

ログイン 非ログイン
インタラクティブ su - [user] したり sshなどでリモートにログイン bashsu <user> で切り替えたシェル
インタラクティブ デスクトップ画面にログイン(--login) スクリプトやコマンド(-c)の実行


Linux の場合、デスクトップ画面からターミナルを起動した場合はインタラクティブ非ログインシェルとなります(~/bashrc読み込み)。

一方、OSX ではターミナル起動時にログインコマンド経由で -bash というコマンド名が指定されるため、インタラクティブログインシェルが起動します(~/bash_profile読み込み)。


初期化ファイルの読み込みが場合分けされるケースがわかったところで、各種環境においてデフォルトで用意される初期化ファイルがどのようになっているかを見てみましょう。


各種環境における初期化ファイル

設定ファイルはディストリビューション毎に様々です。

CentOS Ubuntu OSX を例に、設定ファイルの読み込み部分を抜粋して見て行きます。


CentOSの初期化ファイル

CentOS/etc/profile は以下のように定義されています。

# /etc/profile
# System wide environment and startup programs, for login setup
# Functions and aliases go in /etc/bashrc
...
for i in /etc/profile.d/*.sh /etc/profile.d/sh.local ; do         # (1)
    if [ -r "$i" ]; then
        if [ "${-#*i}" != "$-" ]; then                            # (2)
            . "$i"
        else
            . "$i" >/dev/null
        fi
    fi
done
...

if [ -n "${BASH_VERSION-}" ] ; then                               # (3)
        if [ -f /etc/bashrc ] ; then
                . /etc/bashrc
       fi
fi


(1) で /etc/profile.d/ 以下にあるスクリプトを読み込んでいます。システムワイドな設定を変更する場合、直接 /etc/profile を編集するのではなく、/etc/profile.d/ 以下に設定ファイルを追加してカスタマイズするようになっています。


(2) は呪文でしか無いのですが、Remove Smallest Prefix Pattern で、${-#*i} でシェル起動時に指定されたフラグの一覧から i を除いたものを返します。これをフラグの一覧 $- と比較することで、インタラクティブモードで起動したかどうかを判定しています。

インタラクティブモードの場合は、/etc/profile.d/ 以下の内容をインポート、非インタラクティブモードの場合も同じくインポートしますが、出力を /dev/null して捨てています(非インタラクティブの場合は定石です)。


(3) では $BASH_VERSION で、bash 起動かを判定し(bash の場合バージョン番号が入る)、bash の場合は(ファイル存在チェックの後) /etc/bashrc をインポートしています。


/etc/profile からインポートされる /etc/bashrc は以下のようになっています。

# /etc/bashrc

# System wide functions and aliases
# Environment stuff goes in /etc/profile

# Prevent doublesourcing
if [ -z "$BASHRCSOURCED" ]; then                  # (1)
  BASHRCSOURCED="Y"

  # are we an interactive shell?
  if [ "$PS1" ]; then            # インタラクティブシェル?
  ...
  fi

  if ! shopt -q login_shell ; then                # (2)
    SHELL=/bin/bash
    for i in /etc/profile.d/*.sh; do
        if [ -r "$i" ]; then
            if [ "$PS1" ]; then  # インタラクティブシェル?
                . "$i"
            else
                . "$i" >/dev/null
            fi
        fi
    done
  fi
fi


(1) では2重読み込みを避けるため BASHRCSOURCED という変数でチェックを行っています。


(2) では、bash の組み込みコマンドである shopt で bash のオプションを得られるので、ログインシェルかどうかの判定を行っています。

ログインシェルでは無い場合は、/etc/profile と同様に /etc/profile.d 配下の内容をインポートしています。

/etc/profile から呼ばれた場合は、(/etc/profile はログインシェルから呼ばれるため) /etc/profile.d/ 配下のファイルの読み込みは行われません。


以上でシステムワイドな設定が終わり、続いてユーザ設定の読み込みが行われます。


CentOS では ~/.bash_profile が以下のように用意されています。

# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
        . ~/.bashrc
fi

PATH=$PATH:$HOME/bin
export PATH


~/.bashrc の読み込みと、PATH変数のエクスポートをしているだけです。


~/.bashrc は以下のようになっています。

# .bashrc

# User specific aliases and functions
alias rm='rm -i'
alias cp='cp -i'
alias mv='mv -i'

# Source global definitions
if [ -f /etc/bashrc ]; then
        . /etc/bashrc
fi

alias の設定と、/etc/bashrc の読み込みだけです。

/etc/bashrc は2重読み込みを行わないようになっているため、ログインシェルから /etc/profile が実行された場合は /etc/bashrc の処理は行われません。

一方、非ログインシェルで、インタラクティブシェルとして起動された場合は、~/.bashrc 経由で /etc/bashrc による初期化が行われます。


Ubuntuの初期化ファイル

Ubuntu/etc/profile はどうなっているでしょう。

# /etc/profile: system-wide .profile file for the Bourne shell (sh(1))
# and Bourne compatible shells (bash(1), ksh(1), ash(1), ...).

if [ "${PS1-}" ]; then        # インタラクティブシェル?
  if [ "${BASH-}" ] && [ "$BASH" != "/bin/sh" ]; then        # bash?
    if [ -f /etc/bash.bashrc ]; then        # (1)
      . /etc/bash.bashrc
    fi
  else
    if [ "`id -u`" -eq 0 ]; then
      PS1='# '
    else
      PS1='$ '
    fi
  fi
fi

if [ -d /etc/profile.d ]; then
  for i in /etc/profile.d/*.sh; do
    if [ -r $i ]; then                         # (2)
      . $i
    fi
  done
  unset i
fi


(1) では、インタラクティブシェルで、かつ bash の場合に /etc/bash.bashrc を読み込んでいます。

なお、${PS1-} はシェルの変数展開の ${parameter:-word}${parameter-word}word が空としている書き方です。

シェルスクリプトでは set -u しておくと未定義の変数を使おうとしたときに処理を終了しますが、${PS1-} はこれを回避する書き方です(CentOS では直接 $PS1 としていましたね)。


(2) では CentOS と同じように /etc/profile.d/ 以下のファイルをインポートしています。


ユーザ設定は ~/.profile が用意されています

if [ "$BASH" ]; then          # bash?
  if [ -f ~/.bashrc ]; then
    . ~/.bashrc
  fi
fi

mesg n || true


bash であれば ~/.bashrc の読み込みをしているだけですね。


~/.bashrc は以下のようになっています。

# ~/.bashrc: executed by bash(1) for non-login shells.

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

# some more ls aliases
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'

# Alias definitions.
if [ -f ~/.bash_aliases ]; then    # (2)
    . ~/.bash_aliases
fi


(1) では PS1 変数を調べ、非インタラクティブの場合は return で抜けています。

(2) では alias 用の設定ファイルを読み込んでいます。


macOS(Catalina以前)の初期化ファイル

macOS/etc/profile は以下のようになっています。

# System-wide .profile for sh(1)

if [ -x /usr/libexec/path_helper ]; then
        eval `/usr/libexec/path_helper -s`
fi

if [ "${BASH-no}" != "no" ]; then            # (1)
        [ -r /etc/bashrc ] && . /etc/bashrc
fi


(1) で bash かどうかを調べ、bash の場合に /etc/bashrc を読み込んでいます。


/etc/bashrc は以下のようになっています。

# System-wide .bashrc file for interactive bash(1) shells.
if [ -z "$PS1" ]; then        # (1)
   return
fi

PS1='\h:\W \u\$ '
# Make bash check its window size after a process completes
shopt -s checkwinsize

[ -r "/etc/bashrc_$TERM_PROGRAM" ] && . "/etc/bashrc_$TERM_PROGRAM"  # (2)


(1) で PS1 変数を調べ、非インタラクティブの場合は return で抜けています。

(2) では $TERM_PROGRAM という変数で定義される Apple_Terminal という文字列に該当する、bashrc_Apple_Terminal というターミナル用の設定を読みこんでいます。


最近のMac ではユーザ設定ファイルはデフォルトでは用意されないようです。


macOS(Catalina)の初期化ファイル

macOS Catalina からは zsh がデフォルトのログインシェルおよびインタラクティブシェルになりました。

アップグレード後は、ターミナル起動時に以下のメッセージが表示されます。

The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.


以下のようにすることで zsh をデフォルトに変更できます。

$ chsh -s /bin/zsh


/etc/profile に相当する /etc/zprofile は以下のようになっています。

# System-wide profile for interactive zsh(1) login shells.

# Setup user specific overrides for this in ~/.zprofile. See zshbuiltins(1)
# and zshoptions(1) for more details.

if [ -x /usr/libexec/path_helper ]; then
        eval `/usr/libexec/path_helper -s`
fi


/etc/bashrc に該当する /etc/zshrc は以下のようになっています。

# System-wide profile for interactive zsh(1) shells.

# Setup user specific overrides for this in ~/.zhsrc. See zshbuiltins(1)
# and zshoptions(1) for more details.

# Correctly display UTF-8 with combining characters.
if [[ "$(locale LC_CTYPE)" == "UTF-8" ]]; then
    setopt COMBINING_CHARS
fi

# Disable the log builtin, so we don't conflict with /usr/bin/log
disable log

# Save command history
HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history
HISTSIZE=2000
SAVEHIST=1000

# Beep on error
setopt BEEP

# ...略

# Default prompt
PS1="%n@%m %1~ %# "

# Useful support for interacting with Terminal.app or other terminal programs
[ -r "/etc/zshrc_$TERM_PROGRAM" ] && . "/etc/zshrc_$TERM_PROGRAM"

初期化のシーケンスは bash の場合と同じようになっています。


~/.bash_profile~/.bashrc は以下のような対応になります。

macOS macOS Catalina
~/.bash_profile ~/.zprofile
~/.bashrc ~/.zshrc


各環境におけるデフォルトの初期化ファイル

各環境のユーザ初期化ファイルの内容をまとめると以下のようになります。

~/.profile ~/.bash_profile ~/.bashrc
CentOS なし ~/.bashrc の読み込み、PATHのexport alias 定義、/etc/bashrc 読み込み
Ubuntu なし ~/.bashrc の読み込み alias 定義、~/.bash_aliases 読み込み
macOS なし なし なし

Linux の標準シェルは bash ですし、macOSbash/zsh を標準としているため、~/.profile は定義されていません。


~/.bash_profile から ~/.bashrc を呼ぶのは鉄板ですね。

~/.bashrcインタラクティブ時に読み込まれるため、bash を対話的に使う場合に必要な定義は ~/.bashrc にまとめ(Aliasなど)、~/.bash_profile はログインシェルで読み込まれるため、ログイン時に一度だけ実行すれば良いものを定義すればよさそうですね。 PASH変数は export することで環境変数となり子シェルに引き継がれるので、bash 起動のたびに行うものでもない ということで、~/.bash_profile環境変数を定義すると。

~/.bash_profile は非インタラクティブ時(たとえばGUIによるログイン)にも呼ばれることを考えると、プロンプト設定などももちろん ~/.bashrc に置くのが良いのでしょう。


では、以上を踏まえて、考えうる初期化ファイルの管理方針をいくつか見ていきましょう(なお、ここでは ~/.shrc などは扱いません)。


単一ファイル管理(~/.bashrc)

~/.bash_profile から ~/.bashrc が呼ばれるので、カスタマイズするのは ~/.bashrc だけで良しとする方針です。


f:id:Naotsugu:20191113224042p:plain

# .bash_profile
if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi
# .bashrc
[ -z "$PS1" ] && return    # 非インタラクティブの場合は return

# ここにお好きな設定を記載
# エイリアス・環境変数・シェルオプション・プロンプト設定


どこに何を書けばよいかを迷わなくて済む利点はありますが、~/.bashrc でPATH変数を export した場合に(export PATH="$HOME/hoge/bin:$PATH")、bash 起動毎に同じPATHが連結(home/hoge/bin:home/hoge/binのように)されてしまう点は、実害は無いかもしれませんが、気持ちいいものではありません。

上記 ~/.bashrc ではインタラクティブ時のみ実行のガードを入れていますが、GUI 起動するプログラムなどに環境変数が渡らなくなってしまうのも良くない場合があるでしょう。

これらを気にしないのであれば、管理コストが低いので採用しても良いかもしれません。


標準的管理(環境変数~/.bash_profile)

ログイン時に一度だけ行えば良い環境変数の定義は~/.bash_profileで行い、その他の対話的操作で必要な設定を ~/.bashrc に行う方針です。

おそらくこれが多数の管理方針と思います。


f:id:Naotsugu:20191113231259p:plain

# .bash_profile
if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

# ここに環境変数を定義
export PATH="$HOME/hoge:$PATH"
# .bashrc
[ -z "$PS1" ] && return    # 非インタラクティブの場合は return

# ここにお好きな設定を記載
# エイリアス・シェルオプション・プロンプト設定


ファイルが別れて少し面倒ですが、先の例の問題点は解消されますし、なにより一般的です。

ただ、zsh を使ったり、ログインシェルが sh だったりと、別のシェルを使い分けるのであれば、もう少し厳格な管理が必要になります。


厳格管理(~/.profile 利用)

~/.bash_profile では ~/.profile~/bashrc の読み込みだけを行い、環境変数~/.profile に定義し、bash の対話的操作で必要なものは ~/bashrc に書く方針です。


f:id:Naotsugu:20191113232315p:plain

# .bash_profile
if [ -f ~/.profile ]; then
    . ~/.profile
fi

if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi
# .profile

# ここに環境変数を定義
export PATH="$HOME/hoge:$PATH"
# .bashrc
[ -z "$PS1" ] && return    # 非インタラクティブの場合は return

# ここにお好きな設定を記載
# エイリアス・シェルオプション・プロンプト設定

bash 以外のシェルも考慮して ~/.profile を用意(sh はこのファイルを読みます)します。ここでは他の設定ファイルのインポートのみを行います。

bash 以外の初期化ファイルからも~/.profile をインポートすることで、環境変数が共有できます。

~/.profile にはGUIアプリで使うものや bin/sh で使うもの、シェルの種類に依存しないものを定義します。

そして ~/.bashrc には bashでしか使わない対話的操作で必要なものを定義します(標準出力や標準エラー出力へ書き込む処理は記載しない)。


macOS Catalina の場合

# .zprofile
if [ -f ~/.profile ]; then
    emulate sh -c 'source ~/.profile'
fi

if [ -f ~/.zshrc ]; then
    . ~/.zshrc
fi
# .profile

# ここに環境変数を定義
export PATH="$HOME/hoge:$PATH"
# .zshrc
[ -z "$PS1" ] && return    # 非インタラクティブの場合は return

# ここにお好きな設定を記載
# エイリアス・シェルオプション・プロンプト設定
# 補完など
# autoload -U compinit
# compinit


まとめ

本記事では、bash の初期化ファイルの読み込みについて整理し、いくつかの管理方針を例示しました。

どのように管理すべきかは利用環境毎に自身で判断し、わかりやすくなっていれば問題ないと思いますし、何が正解かもそれぞれです。

本記事が初期化ファイルの運用の一助になれば幸いです。



管理者になる人の はじめてのUNIX

管理者になる人の はじめてのUNIX

新しいLinuxの教科書

新しいLinuxの教科書

UNIXコマンドブック 第4版

UNIXコマンドブック 第4版