5つの用語はそれぞれ別の層を指す

ターミナル、端末エミュレータ、コンソール、TTY、シェルは、日常会話ではどれも「黒い画面」の意味で使われます。実際には、それぞれ別の部品を指しています。この区別がないと、設定をどのファイルに書けばよいのか、不具合の原因がどこにあるのかを判断できません。

用語指すもの例
ターミナル(端末)文字を入力し、結果を表示する装置の総称もとはVT100などの物理的な端末装置
端末エミュレータ物理端末をソフトウェアで再現したGUIアプリGNOME Terminal、Konsole、xterm
コンソールコンピュータに直接つながった入出力装置。Linuxでは仮想コンソールを指すことが多いCtrl+Alt+F3で切り替わる画面
TTYカーネルが端末を扱うためのデバイスファイル/dev/tty3、/dev/pts/0
シェル入力された文字列をコマンドとして解釈し、実行するプログラムbash、zsh、dash

端末エミュレータでキーを押してから文字が出るまで

GNOME Terminalで ls と入力した場合を順に追います。キーボードの信号はまずカーネルが受け取ります。次にディスプレイサーバー(XorgやWaylandのコンポジタ)を経由して、前面にある端末エミュレータに届きます。端末エミュレータはその文字を疑似端末(PTY)に書き込みます。PTYはカーネルがソフトウェアで用意するTTYで、/dev/pts/0 のような名前が付いています。PTYの反対側ではシェルが入力を待っています。Enterが押されるとシェルは1行を受け取り、コマンドとして解釈して実行します。ls の出力は逆の経路でPTYを通り、最後に端末エミュレータがフォントを使って画面に描画します。

この流れから分かるとおり、端末エミュレータは表示の担当、シェルはコマンド実行の担当で、別々のプログラムです。そのため、端末エミュレータをKonsoleに替えてもシェルはbashのまま使えます。シェルをzshに替えても、端末エミュレータは同じものを使い続けられます。文字化けやフォントの問題なら端末エミュレータの設定を、プロンプトやエイリアスの問題ならシェルの設定を見る、という切り分けができます。

TTY自体も、文字を右から左へ流すだけの層ではありません。コマンドの実行中にCtrl+Cを押すと、TTYはこれを文字としてプログラムに渡しません。代わりに、実行中のプロセスへ中断シグナル(SIGINT)を送ります。今どのTTYを使っているかは tty コマンドで確認できます。

$ tty
/dev/pts/0

Ctrl+Alt+F3で出る黒い画面は仮想コンソール

デスクトップ環境でCtrl+Alt+F3を押すと、GUIが消えてログインプロンプトだけの画面に切り替わります。これはLinuxカーネルが提供する仮想コンソールです。1組のキーボードとディスプレイを、複数の独立した端末として切り替えて使う仕組みです。各画面は /dev/tty1、/dev/tty2 といったデバイスに対応しています。

仮想コンソールでは、ディスプレイサーバーも端末エミュレータも動いていません。キー入力はカーネル内のTTYに直接届き、文字の描画もカーネルが行います。そのため、GUIが固まったときやグラフィックドライバが壊れたときでも、ここからログインして復旧作業ができます。ログインを受け付けるのはagettyとloginというプログラムです。認証に成功するとシェルが起動します。

GUIのセッションが何番の仮想コンソールで動いているかは、ディストリビューションやディスプレイマネージャによって異なります。GUIに戻れないときは、Ctrl+Alt+F1、F2、F7を順に試してください。

.bashrcと.profileが読み込まれる場面の違い

シェルの起動のされ方には「ログインシェル」と「非ログインシェル」の区別があります。ログインシェルは、ユーザー名とパスワードでログインした直後に起動するシェルです。仮想コンソールやSSHでのログインがこれに当たります。非ログインシェルは、ログイン済みの状態から追加で開くシェルです。GUIの端末エミュレータで開くウィンドウやタブは、多くの場合こちらです。bashは起動のされ方によって、読み込む設定ファイルを変えます。

起動のされ方例bashが読むファイル
ログインシェル仮想コンソール、SSH/etc/profile を読み、続いて ~/.bash_profile、~/.bash_login、~/.profile のうち最初に見つかった1つだけを読む
対話的な非ログインシェル端末エミュレータの新しいタブ~/.bashrc(Debian/Ubuntuでは /etc/bash.bashrc も読む)
非対話シェルシェルスクリプトの実行原則としてどちらも読まない

この違いは「.bashrcに書いたエイリアスがSSHでは効かない」「.bash_profileを作ったら.profileの設定が消えた」というトラブルとして現れます。前者は、ログインシェルが ~/.bashrc を直接読まないことが原因です。後者は、~/.bash_profile が見つかった時点で ~/.profile が読まれなくなることが原因です。Debian/Ubuntuの初期状態の ~/.profile には、bashで動いている場合に ~/.bashrc を読み込む記述が入っています。自分で ~/.bash_profile を作るときは、同じ処理を書き足しておきます。

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

書き分けの目安として、PATHなどの環境変数はログイン時に1回読まれる ~/.profile に置きます。エイリアスやプロンプトなど対話操作の設定は ~/.bashrc に置きます。GUIでログインしたときに ~/.profile が読まれるかどうかは、ディスプレイマネージャやデスクトップ環境によって異なります。今のシェルがログインシェルかどうかは shopt login_shell で確認できます。zshではファイル名が変わり、~/.zprofile はログイン時に、~/.zshrc は対話シェルの起動時(ログインシェルを含む)に読まれます。

#!/bin/shのスクリプトがDebian/Ubuntuでbashと違う動きをする理由

shは特定のプログラムの名前ではありません。POSIXという規格で定められた標準シェルの呼び名です。/bin/sh の実体として何を置くかは、ディストリビューションが決めています。Debian/Ubuntuでは、/bin/sh はdashへのシンボリックリンクです。dashは起動が速い代わりに、POSIXの範囲外の機能をほとんど持っていません。一方、Fedoraなどでは /bin/sh はbashを指しています。実体は次のコマンドで確認できます。

$ readlink -f /bin/sh
/usr/bin/dash

出力例はUbuntuのものです。このため、bashでしか使えない書き方(bashismと呼ばれます)を含むスクリプトの1行目に #!/bin/sh と書くと、Fedoraでは動くのにUbuntuではエラーになります。

#!/bin/sh
if [[ $1 == yes ]]; then
    echo ok
fi

これをdashで実行すると「[[: not found」のようなエラーになります。[[ ]] はbashの拡張構文なので、dashには通じません。配列、{1..5} のような連番展開、source コマンドもdashでは使えません(POSIXでは source の代わりに . を使います)。echo -e のように、エラーにはならず出力だけが変わる例もあります。dashの echo は -e をオプションとして扱わず、そのまま文字として表示します。

また、sh script.sh のようにシェルを指定して実行すると、1行目の #! は無視されます。#!/bin/bash と書いたファイルでも、この方法で起動すればUbuntuではdashで実行されます。

トラブルの原因を層ごとに切り分ける

不具合に出会ったら、まずどの層の問題かを判断します。文字化けや表示崩れなら端末エミュレータを確認します。設定が反映されないなら、シェルがログインシェルとして起動しているかどうかを確認します。スクリプトの動きが環境によって違うなら、/bin/sh の実体と実行方法を確認します。スクリプトを書くときは、bashの機能を使うなら1行目を #!/bin/bash にし、./script.sh のように実行します。複数のディストリビューションで動かす必要があるなら、#!/bin/sh のままPOSIXの範囲で書きます。その場合は ShellCheck などの静的解析ツール(実行せずにコードを検査するツール)を使うと、bashismを事前に見つけられます。