cd ..のようなコマンドを入力するとき、そこにはターミナルとシェルという2つのものが関わっています。しかし、どちらがどちらでしょうか?これらは同義語なのでしょうか?別物なら、どのように連携しているのでしょうか?
要点
- ターミナルとシェルは別物
- 様々なターミナルと様々なシェルが存在
- ターミナルとシェルは擬似端末(pty)を介して通信
- SSHやWSLではチェーンされた複数のptyを介してターミナルとシェルが複数のシステムに跨って通信可能
- シェルはコマンドインタプリタでもあり、スクリプトインタプリタでもある
- シェルにはPOSIX標準規格がある
背景
私は普段Windowsで仕事をしており、PowerShellやWSLを日常的に使用しています。しかし、最近までPowerShellが何であるかあまり考えたことはありませんでした。コマンドラインインターフェースを提供する「あのもの」だと漠然と思い込んでいたのです。特に複数のWSL Linuxディストリビューションが存在する中で、なぜPowerShellがすべてのLinuxコマンドを理解できるのかは謎でした。
結局、私はターミナルとシェルという概念を混同していたのです。
以下、学んだことをまとめていきます。
ターミナルは人間との窓口、シェルはコマンドのインタプリタ
ターミナルとシェルは異なるものです。
後述するように、ターミナルはコマンド以外の入力も受け取れますし、シェルもコマンド解釈以上のことをしますが、ここでは、人間がコマンドを入力して、シェルがそれを解釈するシンプルなシナリオを想定しましょう。
- ターミナル:コマンドを入力し、その出力を表示する役割を担います
- シェル:入力されたコマンドを解釈するプログラムで、人間がOSと対話することを可能にします
ターミナルは人間がシェルと対話するための「窓口」であり、今日では、より正確に言うとターミナルエミュレータと呼ばれるプログラムです。「エミュレータ」と呼ばれるのは、元々ターミナルが VT100 のような物理的なI/Oデバイスだったからです。エミュレータとは、ハードウェアを模倣するソフトウェアのことです。私たちは今、画面上でその物理的な端末をエミュレートするプログラムを使用しています。
一方、シェルは単なるプログラムです。
画面に表示されているウィンドウがターミナルです。一方、シェルは目に見えるものではありません。
様々なターミナル
デスクトップ環境を持つLinux(例:Ubuntuの場合はGNOME)には、デフォルトのターミナルエミュレータがありますが、AlacrittyやKittyやxtermといった他のものもインストール可能です。
デスクトップ環境がないLinuxの場合、画面全体がテキストモードの端末(TTYまたは仮想コンソールと呼ばれる)になります。
豆知識: TTYはTeleTYpewriter(テレタイプライター)の略です。これはコンピューターに接続されたタイプライターのようなデバイスでした。この名称はデバイスが廃れた後も残っています。
テレタイプライターは物理的なターミナルよりもさらに古いです。出力に、テレタイプライターは紙を使用し、ターミナルはスクリーンを使用していました。
macOSにはデフォルトのターミナルアプリケーションがあります。WindowsにはWindows TerminalとWindows Console Hostがあります。どちらのOSでもサードパーティ製のターミナルをインストールできます。
様々なシェル
シェルは単なるシステムにインストールされたプログラムです。システムには複数のシェルがインストールされている場合もあれば、シェルがまったくない場合もあります(例:IoTデバイス)。もちろん、シェルのないシステムは人間が使用するには不便です。
Unix系システムでは、bash、zsh、fishなどが一般的です。WindowsにはcmdとPowerShellがあります。
OSは新規ユーザー向けのデフォルトシェルが付属しています。例えば、ほとんどのLinuxディストリビューションではbash、macOS(macOS Catalina 10.15以降)ではzshです。これはシステムレベルまたは特定のユーザーアカウントで変更できます。
簡単な例で複数のシェルとの対話を示します。
この例では、bashはuser@hostname:current_working_directory>を表示し、zshはhostname%を表示しています。このプロンプトの違いは単なる設定です。また、bashが>、$、または他の何かを表示するかは設定可能です。この例ではWSL openSUSEを使用しており、このディストリビューションのデフォルト設定では>を使用しています。
# bashがデフォルトシェルの状態で開始
me@my_pc:~> echo $0 # 現在対話しているシェルを取得
-bash
me@my_pc:~> echo $SHELL # デフォルトシェルを取得(現在のシェルとは限らない)
/bin/bash # デフォルトシェルはbash
# zshを一時的に起動
me@my_pc:~> zsh # zshは単なる別のプログラム
my_pc% echo $0 # 再度現在のシェルを取得
zsh # 現在のシェルはzsh
my_pc% echo $SHELL
/bin/bash # デフォルトはまだbash
my_pc% exit # zshを終了し、bashに戻る
# デフォルトシェルをzshに変更
me@my_pc:~> chsh -s /bin/zsh
Password:
me@my_pc:~> exit # ログアウトして、変更を有効にするために再ログイン
# 再ログイン後
my_pc% echo $0
-zsh
my_pc% echo $SHELL
/bin/zsh # デフォルトシェルはzsh
# bashを一時的に起動
my_pc% bash
me@my_pc:~> echo $0
bash
me@my_pc:~> echo $SHELL
/bin/zsh # デフォルトはまだzsh
me@my_pc:~> exit # bashを終了し、zshに戻る
exit
my_pc%
-bashと-zshのハイフンについて
-bashと-zshのハイフンは現在対話しているシェルがログインシェルであることを示しています。シェルAからシェルBに入り、シェルBを終了してAに戻ることができます。ログイン時に最初に入ったもので、現在のログインセッションで最後に終了できるものがログインシェルです。例で示します。
# シェルはLIFO順でスタックする
my_pc% echo $0
-zsh # ログインシェル(スタックの底)
my_pc% bash # zshの上にbashを起動
me@my_pc:~> echo $0
bash # 非ログインシェル
me@my_pc:~> zsh # bashの上にzshを起動
my_pc% echo $0
zsh # これで3つのシェルの深さ
my_pc% exit # 最も内側のzshを終了 → bashに戻る
me@my_pc:~> exit # bashを終了 → ログインzshに戻る
exit
my_pc% exit # ログインzshを終了 → 完全にログアウト
ターミナルとシェルの互換性は擬似端末によって実現される
ターミナルとシェルは多対多で互換性があります。これを可能にしているのが擬似端末(pseudoterminal、もしくはpseudotty、略してpty)という仕組みです。Unix系システムで長年使われてきた仕組みで、Windowsでも2018年にConPTYとして同様の仕組みが導入されました。
ptyには、リーダー側とフォロワー側があります。この用語は、マスター・スレーブと呼ばれることがより一般的かもしれません。しかし、ここではその用語を使用しません。
- リーダー側にターミナルエミュレータがあり、フォロワー側にシェルがある場合、おなじみのCLIインターフェースができます
- フォロワー側にviやnanoのようなテキストエディタがある場合、ターミナルエミュレータからテキストファイルを編集できます
- SSH接続中は、ローカルマシンのptyのフォロワー側にSSHクライアントがあり、リモートマシンのptyのリーダー側にSSHサーバーがあります
SSH接続の面白いケースについて詳しく説明します。これは、ネットワークを介してローカルマシンとリモートマシンでptyをチェーンすることで実現されます。以下は、人間がリモートシェルと対話する際の基本的なセットアップの例です。
ローカルファイルをターミナルから編集するのと同様に、リモートptyのフォロワー側にテキストエディタがある場合、リモートマシン上のテキストファイル(例えば、サーバー側アプリケーションのdot envファイル)を編集できます。
また、リモートマシンのフォロワー側に別のSSHクライアントを配置すると、リモートマシンを踏み台サーバーとして使用して別のサーバーにホップできます。
WindowsターミナルからLinuxシェルと対話する
SSHの典型的な使用例は、リモートマシンがLinuxマシンである場合です。上記のSSH接続の図は、ローカルターミナルとリモートシェルのOSについて何も仮定していないことに注意してください。ローカルマシンがLinux、macOS、Windowsのいずれであっても関係ありません。 実際、リモートマシンがLinuxだからといって、ローカルマシンにとってLinuxがWindowsより特別なわけではありません。 冒頭で述べた謎は、シェルとターミナルの仕組みを最初理解していれば、そもそも謎に感じることはなかったでしょう。
私は通常、Windows TerminalからSSHクライアントを使用してEC2インスタンスのようなVMに接続します。この用途にPuTTYを使用することも一般的です。PuTTYは、ターミナルエミュレータとSSHクライアントが一体化したものです。
豆知識:PuTTYの「TTY」は前述のTeleTYpewriterから来ています。一方、「Pu」は何の意味もありません。
もう1つの典型的なシナリオは、WSLを使用してWindows TerminalからLinuxシェルと対話する場合です。アーキテクチャはSSHと似ています。Windows側のConPTYがWSL内のLinux ptyにブリッジされ、WSLインフラストラクチャがそれらの間の接続レイヤーとして機能します。
単なるインストールされたプログラムであるシェルをアンインストールしたらどうなる?
シェルは単なるインストールされたプログラムなので、原理的にはアンインストール自体はできます。
アンインストールすると何を失うのでしょうか?1つのシェルをアンインストールすると、OSと対話する方法が1つ減ります。その上、シェルはシェルスクリプトのインタープリタでもあるので、シェルスクリプトを実行する手段も1つ減ります。
例えば、上記のbashとzshの両方がある場合、zshをアンインストールしても単に手段が減るだけで問題ないはずですが、bashをアンインストールすると多くのものが壊れてしまいます。
少なくとも1つのシェルが残っているからと言って大丈夫だとは限りません。多くのプログラムやシステムユーティリティは、シバン #!/bin/sh で始まるシェルスクリプトに依存しています。(具体例の1つとして、cat /usr/bin/zcat を試してみてください。このファイルユーティリティは私のWSL OpenSUSEに付属しています。)
カーネルがシバン #!/bin/sh で始まるファイルを実行しようとすると、カーネルは実際にスクリプトのファイル名を引数として /bin/sh を実行します。
これはシェルの機能ではなく、カーネルの機能です!
しかし、/bin/sh は通常、実際のシェルへのシンボリックリンクです。私のWSL OpenSUSEでは:
my_pc% ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Sep 30 00:32 /bin/sh -> bash
Linuxディストリビューションによって、異なるシェルを指すこともあります。
リンク先をアンインストールすると /bin/sh が壊れたシンボリックリンクになり、システムユーティリティ、パッケージマネージャー、起動スクリプトなど、多くの重要なものが壊れてしまうでしょう。
POSIXによるシェルの共通語
前述通り、Unix系システムでは:
- 様々なシェルが存在する
- シェルスクリプトは多くの場合シバン
#!/bin/shで始まる -
/bin/shは通常、実際のシェルへのシンボリックリンクである - しかし、リンク先のシェルはLinuxディストリビューションによって異なる
では、どのようにして互換性が保たれているのでしょうか?
POSIX (Portable Operating System Interface) というOS間の互換性を維持するための標準規格群があります。その一部として、「標準的な」シェルコマンド言語の構文・意味論を規定しています。
- POSIX規格群の中核はPOSIX.1です(2025-11-29現在、最新版はPOSIX.1-2024)
- POSIX.1はIEEE、The Open Group、およびISOによって共同開発されています
- POSIX.1-2024は同じ内容が2つの名称で公開されています:IEEE版「IEEE Std 1003.1-2024」とThe Open Group版「The Open Group Standard Base Specifications, Issue 8」
- IEEEの版へのアクセスを有料としていますが、The Open Groupの版は無料で公開されています
「Shell & Utilities」の巻では、シェルの動作が規定されています。
例として、#!/bin/sh で始まるスクリプトがLinuxディストリビューションによって、リンク先が変わっても、一貫して動作する理由を理解するために「3. Utilities」セクションを見てみましょう。「sh」の項目には、次のように記載されています。
The sh utility is a command language interpreter that shall execute commands read from a command line string, the standard input, or a specified file. The application shall ensure that the commands to be executed are expressed in the language described in 2. Shell Command Language.
つまり、POSIX準拠のシステムでは:
-
shコマンドが必ず存在 - それは「2. Shell Command Language」セクションに従って動作するコマンド言語インタプリタでなければならない
ただし、次のようにも述べられています:
Applications should note that the standard PATH to the shell cannot be assumed to be either /bin/sh or /usr/bin/sh, and should be determined by interrogation of the PATH returned by getconf PATH , ensuring that the returned pathname is an absolute pathname and not a shell built-in.
POSIX準拠のシェルスクリプトは#!/bin/shで始まれば、shへのパスが/bin/shである場合に一貫した動作が期待できます。
シェルがすべてPOSIX準拠だと仮定してはいけません!例えば、fishは「意図的にPOSIX非準拠」です。
参考文献
- RFC 4254 - The Secure Shell (SSH) Connection Protocol - SSH接続プロトコルの公式仕様書
- Windows Command-Line: Introducing the Windows Pseudo Console (ConPTY) - MicrosoftによるConPTYの紹介記事
- pty(7) - Linux manual page - Linuxの擬似端末に関するマニュアル
- The Open Group Base Specifications Issue 8 (POSIX.1-2024) - POSIX.1仕様書