@@ -11,33 +11,29 @@ author: "midori"
1111「え、こんなの使うの?ほんとにコマンド打っていいの?」
1212
1313GUI(グラフィカルユーザーインターフェース)[ ^ 1 ] しか使ったことのない私にとって、あの黒い画面に意味の分からない文字列を打ち込み、エンターを押す瞬間は毎回緊張する体験でした。
14-
15- エンジニアとして働き始めた後も、Google Cloud Platform(GCP)やGitを操作するとき、経験豊富なエンジニアほどCLIを好んで使っています。ジュニアの私にとっては、操作対象が目に見えず、GUIのように直感的ではないCLIは、高度で難しいものに感じられ、CLIを使いこなすエンジニアは憧れの対象でもありました。
14+ エンジニアとして働き始めた後も、Google Cloud Platform(GCP)やGitを操作するとき、経験豊富なエンジニアほどCLIを好んで使っています。ジュニアの私にとっては、操作対象が目に見えず、GUIのように直感的ではないCLIは、高度で難しいものに感じられ、CLIを使いこなすエンジニアは憧れの対象でもありました。(Vimerほんとかっこいい)
1615
1716ところが近年、AIの普及によってCLIを補助するツールが続々と登場しています。自然言語で会話するだけで、ChatGPTやGitHub Copilotがコマンドを生成してくれたり、補完や自動化を助けてくれたりするようになりました。これにより、ターミナルは「コマンドを入力する黒い画面」から「AIと人間が協力して使う対話の場」へと進化しつつあります。結果として、ターミナルへの心理的なハードルもぐっと下がり、身近な存在になり始めています。
1817
19- しかし、そこで立ち止まって考えたいのが「そもそもターミナルはどう動いているのか?」という基礎です。ターミナルは表面的にはシンプルですが、実は50年以上基本構造が変わっていないようです。その裏側にはOSやシェルの仕組みと密接に関わっています 。この仕組みを知ることは、AIとの未来を考えるうえでも大切です 。
18+ しかし、そこで立ち止まって考えたいのが「そもそもターミナルはどう動いているのか?」という基礎です。ターミナルは表面的にはシンプルですが、実は50年以上基本構造が変わっていないようです。その裏側はOSやシェルの仕組みと密接に関わっています 。この仕組みを知ることは、AIのような新しい技術と付き合っていく上でも重要だと考えました 。
2019
2120本記事では以下を目指します:
2221
23221 . ** ターミナルの歴史を知る**
24232 . ** ターミナルの仕組みを理解する**
25- 3 . ** TypeScriptでシンプルな擬似実装を行い、動きを直感的に理解する**
26-
27- ---
24+ 3 . ** TypeScriptでシンプルな擬似実装を行い、ターミナルの仕組みを実際に確認する**
2825
2926[ ^ 1 ] : グラフィカルユーザーインターフェースとは、マウスやアイコンなどを用いて直感的に操作できる仕組みのことです。
3027
3128[ ^ 2 ] : インターフェースとは、人と機械が情報をやり取りする窓口のことです。ここでは「AIと人間が同じCLIを通じて対話する関係」を指しています。
3229
33- ---
3430
3531## 2. ターミナルの歴史
3632そもそも、ターミナルとは何でしょうか。調べてみると、コンピューターの歴史的な変遷と共に「コンピュータと人をつなぐ入出力装置」として進化してきたことが分かりました。まずはその歴史を見ていきたいと思います。
3733
3834### 2.1 パンチカードの時代
3935
40- コンピューターが誕生したばかりの1950年代、プログラムの入力は 「パンチカード」と呼ばれる厚紙に穴を開けるという方法でした。複数枚のカードを束ねてリーダーに通すと、一度に処理を実行できる仕組みです 。
36+ 1950年代、コンピューターへのプログラム入力は 「パンチカード」と呼ばれる厚紙に穴を開ける方法で行われていました。複数枚のカードを束ね、カードリーダーに通すと一度に処理が実行されます 。
4137
4238この時代のコンピューターは人間が即時に操作し、コンピューターが対話的に応答する仕組みは存在しませんでした。人間がカードを準備し、コンピューターが処理を行った後に、結果を後で紙に印刷するというサイクルで動いており、これは** バッチ処理** [ ^ 3 ] と呼ばれました。
4339
@@ -65,11 +61,7 @@ GUI(グラフィカルユーザーインターフェース)[^1]しか使っ
6561UNIXの大きな特徴のひとつが、「すべてをファイルとして扱う」 という設計思想です。
6662
6763ここでいう「ファイル」とは、単にハードディスク上の文書や画像だけではありません。
68- キーボード
69- ディスプレイ
70- プリンタ
71- 通信ポート
72- といった 入出力装置までも「特別なファイル」 として表現されました。
64+ キーボード、ディスプレイ、プリンタ、通信ポートといった 入出力装置までも「特別なファイル」 として表現されました。
7365端末も例外ではなく、` /dev/tty ` というファイルを通じて利用されます。` dev ` は` device ` 、` tty ` は` teletypewriter ` の略です。
7466プログラムは ` /dev/tty ` に書き込めば端末に文字が表示され、読み込めばユーザーのキー入力を得られる仕組みです。
7567
@@ -78,7 +70,6 @@ UNIXの大きな特徴のひとつが、「すべてをファイルとして扱
7870### 2.3 ディスプレイ端末と制御コード
79711970年代後半には、紙ではなくディスプレイに文字を映すビデオ端末が普及します。代表例が1978年登場の VT100 です。
8072VT100は制御コード(ANSIエスケープ)を導入し、アプリは文字列を出力するだけで端末がカーソル移動・色付け・画面消去を実行できるようになりました。
81- 端末ごとの差異(“方言”)はtermcap/terminfoといった抽象化レイヤが吸収し、アプリは「やりたいこと」を宣言し、ライブラリが「端末固有の制御列」に翻訳する形へ収れんしました。
8273この仕組みは、現代の ` ls --color ` やプログレスバー表示にも通底しています。
8374![ By Jason Scott - Flickr: IMG_9976, CC BY 2.0, https://commons.wikimedia.org/w/index.php?curid=29457452 ] ( https://storage.googleapis.com/zenn-user-upload/5f56c5387dcf-20250908.jpg ) {style="width:90%;"}
8475
@@ -322,7 +313,7 @@ ls > result.txt
322313### 4.2 実装
323314
324315
325- | ラベル | 要素名 | これは何? | コード上の実体 | 何の代わり?(狙い) |
316+ | | 要素名 | これは何? | コード上の実体 | 何の代わり?(狙い) |
326317| ------ | -------------------- | -------------------------- | ----------------------------------------------------------- | ---------------------------------------------------- |
327318| ** 1** | ** PtyPair** | 入出力の** 配線アダプタ** (人↔シェルの往復) | ` class PtyPair { masterIn/masterOut/slaveIn/slaveOut } ` | 本物の ** Pseudo TTY(PTY)** の簡易再現(` PassThrough ` でメモリ内パイプ) |
328319| ** 2** | ** Shell** | ** REPL 本体** (1行読む→解釈→実行→表示) | ` class MiniShell { run(); tokenize(); } ` | ** bash/zsh** の最小版(まずは行単位でディスパッチのみ) |
@@ -343,10 +334,15 @@ import { setTimeout as sleepMs } from "node:timers/promises";
343334
344335// -----------------------------
345336// 1) 擬似端末 (PTY) ペア
337+ // - master*: 人間(端末)側
338+ // - slave*: シェル側
339+ // 2方向のデータ流れを PassThrough で再現します
346340// -----------------------------
347341class PtyPair {
348- masterIn = new PassThrough (); // Terminal → masterIn → slaveIn
349- masterOut = new PassThrough (); // slaveOut → masterOut → Terminal
342+ // Terminal → masterIn → (pipe) → slaveIn → Shell
343+ masterIn = new PassThrough ();
344+ // Shell → slaveOut → (pipe) → masterOut → Terminal
345+ masterOut = new PassThrough ();
350346 slaveIn = new PassThrough ();
351347 slaveOut = new PassThrough ();
352348
@@ -359,8 +355,11 @@ class PtyPair {
359355}
360356
361357// -----------------------------
362- // 2) ユーティリティ(トークン化)
358+ // ユーティリティ(トークン化)
363359// - クォート対応の軽量パーサ(超簡易)
360+ // - 空白で区切るが、' ' や " " で囲まれた空白は1つの単語として扱う
361+ // - 例: tokenize(`echo "hello world"`) → ["echo","hello world"]
362+ // - 厳密なシェル構文ではなく最小仕様
364363// -----------------------------
365364function tokenize(line : string ): string [] {
366365 const tokens: string [] = [];
@@ -397,8 +396,15 @@ function tokenize(line: string): string[] {
397396
398397// -----------------------------
399398// 3) Builtin Commands
399+ // - ここに {名前: 関数} を追加すると、新しいコマンドが増える
400+ // - Cmd は「引数と入出力インターフェースを受け、終了コードを返す」関数
400401// -----------------------------
401- type IO = { stdin: Readable ; stdout: Writable ; stderr: Writable ; prompt: () => void };
402+ type IO = {
403+ stdin: Readable ; // コマンドの標準入力(今は空。将来パイプ実装で活きる)
404+ stdout: Writable ; // コマンドの標準出力
405+ stderr: Writable ; // コマンドの標準エラー出力
406+ prompt: () => void ; // プロンプト再表示用のコールバック(必要時に呼ぶ)
407+ };
402408type Cmd = (args : string [], io : IO ) => Promise <number > | number ;
403409
404410const commands: Record <string , Cmd > = {
@@ -416,7 +422,7 @@ const commands: Record<string, Cmd> = {
416422 " Tips: パイプやリダイレクトは未対応(最小実装)" ,
417423 ].join (" \n " ) + " \n "
418424 );
419- return 0 ;
425+ return 0 ; // 終了コード0 = 成功
420426 },
421427
422428 echo : async (args , io ) => {
@@ -455,12 +461,18 @@ const commands: Record<string, Cmd> = {
455461};
456462
457463// -----------------------------
458- // 4) シェル(REPL)
464+ // 2) シェル(REPL)
465+ // - 1行読み取って → tokenize → commands から関数を引き当てて実行
466+ // - 未定義コマンドはエラーメッセージを出して次のプロンプトへ
459467// -----------------------------
460468class MiniShell {
461469 private rl: readline .Interface ;
462470
463- constructor (private slaveIn : Readable , private slaveOut : Writable , private slaveErr : Writable ) {
471+ constructor (
472+ private slaveIn : Readable , // シェルが読む(人間から来る)入力
473+ private slaveOut : Writable , // シェルが書く(人間へ返す)出力
474+ private slaveErr : Writable // エラー表示用
475+ ) {
464476 this .rl = readline .createInterface ({
465477 input: this .slaveIn ,
466478 output: this .slaveOut ,
@@ -473,17 +485,22 @@ class MiniShell {
473485 this .slaveOut .write (" > " );
474486 }
475487
488+ // メインループ:1行ずつ処理
476489 async run() {
477490 this .prompt ();
478491 for await (const line of this .rl ) {
479492 const trimmed = line .trim ();
480493 if (! trimmed ) {
494+ // 空行は何もしないで次のプロンプト
481495 this .prompt ();
482496 continue ;
483497 }
498+
484499 const [cmd, ... args] = tokenize (trimmed );
485500 const impl = commands [cmd ];
501+
486502 if (! impl ) {
503+ // 未知コマンド
487504 this .slaveErr .write (` command not found: ${cmd }\n ` );
488505 this .prompt ();
489506 continue ;
@@ -495,22 +512,27 @@ class MiniShell {
495512 stdin .end ();
496513
497514 try {
515+ // コマンドを実行
498516 await impl (args , {
499517 stdin ,
500518 stdout: this .slaveOut ,
501519 stderr: this .slaveErr ,
502520 prompt : () => this .prompt (),
503521 });
504- } catch (e : any ) {
505- this .slaveErr .write (` error: ${e ?.message ?? String (e )}\n ` );
522+ } catch (e : unknown ) {
523+ const msg = e instanceof Error ? e .message : String (e );
524+ // コマンドの実行時エラーを握りつぶさず通知
525+ this .slaveErr .write (` error: ${msg }\n ` );
506526 }
507527 this .prompt ();
508528 }
509529 }
510530}
511531
512532// -----------------------------
513- // 5) Terminal(人間側の出入口)
533+ // 4) Terminal(人間側の出入口)
534+ // - キーボード入力を masterIn に流し、Shell の出力を stdout に表示
535+ // - Ctrl+C は「いまの行を諦めて次のプロンプトへ」という簡易挙動
514536// -----------------------------
515537async function main() {
516538 const pty = new PtyPair ();
@@ -581,19 +603,38 @@ npx tsx mini-terminal.ts
581603
582604> これは、最小実装なので ** パイプ(` | ` )やリダイレクト(` > ` )** は未対応です。
583605
584- 文字数と締め切りにより、ここではどのような挙動をするかあえて解説しません。
606+ 文字数と締め切りにより、ここではどのような挙動をするかあえて解説しません()
585607もし良ければ、ぜひ試してみてください。
586608
587- ---
588609
589610### 4.4 まとめ
590611この実装からも、ターミナルは「入力と出力を結ぶ通話路」であり、シェルはその入力を解釈してプログラムへ渡す受付係として機能します。そして、Ctrl+C のような割り込みも、本質的には「特別な文字列を入力ストリームに流す仕組み」として説明できます。
591612
592613
593614### 5. 最後に
594- 今回この記事を書いたことで、黒い画面の奥にある奥深さと歴史をあらためて感じることができました 。(もう怖くないぞ!)
595- とりわけ強く実感したのは、「シンプルであること」の凄さです。コンピュータやOSには流行り廃りがありますが、その中でターミナルは何十年も利用され続けてきました。そして現代、AI時代を迎えた今でも 、その存在はむしろ再評価され、最盛を迎えています。
615+ 今回この記事を書いたことで、黒い画面の奥深さを感じることができました 。(もう怖くないぞ!)
616+ とりわけ強く実感したのは、「シンプルであること」の凄さです。コンピュータやOSには流行り廃りがありますが、その中でターミナルは何十年も利用され続けてきました。そしてAI時代を迎えた今でも 、その存在はむしろ再評価され、最盛を迎えています。
596617
597- これは、ターミナルが複雑な機能を詰め込んだからではありません。むしろ逆で、「文字をやり取りする」という最小限の仕組みに徹しているからこそ、どんな時代の技術とも接続でき、使い続けられるのです。
598- シンプルであることは一見地味ですが、それこそが長く生き残る力を持ち、普遍性を与えるのだと感じました。
618+ これは、ターミナルが複雑な機能を詰め込んだからではありません。むしろ逆で、「文字をやり取りする」「全てをファイルとみなす」 という最小限の仕組みに徹しているからこそ、どんな時代の技術とも接続でき、使い続けられるのです。
619+ シンプルであることは一見地味ですが、それこそが長く生き残る力を持ち、普遍性を与えるのだと感じました。今後エンジニアとして、このような普遍性があり長く愛されるサービスを作る上でも大事にしたいなと思いました。
599620「黒い画面が怖い」あなたに、この記事を通して共感してもらえたら幸いです。
621+
622+ ### 参考文献
623+ Gregory Anders, “State of the Terminal,” g.p. anders blog, March 12, 2024.
624+ URL: https://gpanders.com/blog/state-of-the-terminal/
625+ (参照日:2025年10月)
626+
627+ Linus Åkesson, “The TTY demystified,” linusakesson.net – Programming,History セクションほか.
628+ URL: https://www.linusakesson.net/programming/tty/
629+ (参照日:2025年10月)
630+
631+ LPI Learning Materials, “103.4_01: Input, Output, Storage,” LPI Learning Portal.
632+ URL: https://learning.lpi.org/en/learning-materials/101-500/103/103.4/103.4_01/
633+ (参照日:2025年10月)
634+
635+ 武内 覚, 試して理解 Linux のしくみ — 実験と図解で学ぶ OS、仮想マシン、コンテナの基礎知識【増補改訂版】, 技術評論社, 2022年10月刊行
636+ https://amzn.asia/d/ceU9Fjn
637+
638+ R. Koucha, “PTY / Pseudo-Terminal (PTY, PDIP) — Redirection of Standard Input and Outputs of a Process,” Tech Corner.
639+ URL: https://www.rkoucha.fr/tech_corner/pty_pdip.html
640+ (参照日:2025年10月)
0 commit comments