[インデックス 10970] ファイルの概要
このコミットは、Go言語の実験的なexp/terminal
パッケージに対して行われた複数のクリーンアップと機能改善をまとめたものです。主な目的は、ターミナルとのインタラクションをより堅牢かつ柔軟にし、アプリケーションがターミナル機能をより容易に利用できるようにすることにありました。具体的には、エスケープコードの組み込み、オートコンプリート機能のコールバック追加、長い入力行の処理改善、Write()
メソッドの挙動修正、パスワード入力時のエコー抑制、ターミナルサイズの取得機能追加などが含まれます。
コミット
commit 7350c771f89e1a068677121341908a8846905c2c
Author: Adam Langley <agl@golang.org>
Date: Thu Dec 22 11:23:57 2011 -0500
exp/terminal: several cleanups
1) Add EscapeCodes to the terminal so that applications don't wire
them in.
2) Add a callback for auto-complete
3) Fix an issue with input lines longer than the width of the
terminal.
4) Have Write() not stomp the current line. It now erases the current
input, writes the output and reprints the prompt and partial input.
5) Support prompting without local echo in Terminal.
6) Add GetSize to report the size of terminal.
R=bradfitz
CC=golang-dev
https://golang.org/cl/5479043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7350c771f89e1a068677121341908a8846905c2c
元コミット内容
exp/terminal: several cleanups
1) Add EscapeCodes to the terminal so that applications don't wire
them in.
2) Add a callback for auto-complete
3) Fix an issue with input lines longer than the width of the
terminal.
4) Have Write() not stomp the current line. It now erases the current
input, writes the output and reprints the prompt and partial input.
5) Support prompting without local echo in Terminal.
6) Add GetSize to report the size of terminal.
R=bradfitz
CC=golang-dev
https://golang.org/cl/5479043
変更の背景
このコミットは、Go言語のexp/terminal
パッケージがまだ実験段階にあった2011年に行われたものです。このパッケージは、GoアプリケーションがVT100互換ターミナルと対話するための低レベルな機能を提供することを目的としていました。当時の実装にはいくつかの課題があり、より堅牢で使いやすいターミナルインタラクションを実現するために、以下の変更が必要とされました。
- エスケープコードの標準化: ターミナルの色やスタイルを変更するためのエスケープシーケンスは、アプリケーション側でハードコードされることが多く、再利用性や保守性に問題がありました。これをパッケージ内で標準化し、
EscapeCodes
として提供することで、アプリケーション開発者が直接エスケープシーケンスを記述する手間を省き、より安全に利用できるようにする必要がありました。 - オートコンプリートのサポート: コマンドラインインターフェース(CLI)において、オートコンプリートはユーザーエクスペリエンスを大幅に向上させる重要な機能です。しかし、当時の
exp/terminal
パッケージには、この機能を実現するためのフックがありませんでした。アプリケーションが独自のオートコンプリートロジックを組み込めるように、コールバックメカニズムの導入が求められました。 - 長い入力行の表示問題: ターミナルの幅を超える長い入力行が正しく表示されない、またはカーソル位置がずれるといった問題が存在しました。これはユーザーが入力する際の視認性や操作性に直結するため、修正が必要でした。
Write()
メソッドの挙動改善: ターミナルに何かを出力する際に、ユーザーが現在入力中の行が上書きされてしまうという問題がありました。これは、非同期でログが出力される場合などにユーザーの入力が失われる原因となるため、現在の入力行を保護しつつ出力を表示し、その後で入力行を再描画するような賢い挙動が求められました。- パスワード入力のサポート: パスワードなどの機密情報を入力する際には、入力文字がターミナルに表示されない「エコーなし」のモードが必要です。この機能が
Terminal
構造体自体に組み込まれていなかったため、追加する必要がありました。 - ターミナルサイズの取得: ターミナルの幅と高さをプログラムから取得できる機能は、動的なレイアウト調整や、ターミナルサイズに応じた表示の最適化を行う上で不可欠です。この機能が不足していたため、追加されました。
これらの改善は、exp/terminal
パッケージをより実用的なものにし、Goで堅牢なCLIアプリケーションを構築するための基盤を強化することを目的としていました。
前提知識の解説
このコミットの変更内容を理解するためには、以下の概念について基本的な知識があると役立ちます。
- VT100ターミナルとエスケープシーケンス: VT100は、DEC(Digital Equipment Corporation)が開発したビデオディスプレイターミナルのモデルであり、その制御シーケンス(エスケープシーケンス)は、現代の多くのターミナルエミュレータのデファクトスタンダードとなっています。エスケープシーケンスは、
ESC
(エスケープ文字、ASCIIコード27)で始まり、その後に続く文字によってカーソル移動、色の変更、画面クリアなどの特殊な操作をターミナルに指示します。例えば、ESC[31m
は前景色を赤に設定するエスケープシーケンスです。 - ローカルエコー (Local Echo): ターミナルにおいて、ユーザーがキーボードから入力した文字が即座に画面に表示される機能を「ローカルエコー」と呼びます。通常、CLIではこのエコーが有効になっていますが、パスワード入力時など、入力内容を隠したい場合にはエコーを無効にする必要があります。
- オートコンプリート (Auto-complete): ユーザーが入力している途中で、システムが残りの入力を予測し、候補を提示する機能です。CLIでは、コマンド名やファイルパスの入力を補完する際によく利用されます。
- Go言語の
exp
パッケージ: Go言語の標準ライブラリには、exp
(experimental)というプレフィックスを持つパッケージ群が存在しました(現在は多くが標準パッケージに昇格したり、廃止されたりしています)。これらは、まだ安定版ではないが、将来的に標準ライブラリに組み込まれる可能性のある実験的な機能を提供していました。exp/terminal
もその一つで、開発途上であり、APIが変更される可能性がありました。 syscall
パッケージとioctl
: Go言語のsyscall
パッケージは、オペレーティングシステムが提供する低レベルなシステムコールにアクセスするための機能を提供します。ioctl
(Input/Output Control)は、Unix系システムでデバイスの制御パラメータを設定・取得するためのシステムコールです。ターミナルのサイズ(幅と高さ)を取得する際などに利用されます。sync.Mutex
: Go言語におけるミューテックス(相互排他ロック)の実装です。複数のゴルーチンが共有リソース(この場合はターミナルの状態)に同時にアクセスする際に、データの競合を防ぎ、安全な並行処理を実現するために使用されます。
技術的詳細
このコミットで行われた技術的な変更は多岐にわたりますが、それぞれがターミナルインタラクションの品質と柔軟性を向上させています。
-
EscapeCodes
構造体の追加とVT100エスケープコードの定義:terminal.go
にEscapeCodes
という新しい構造体が追加されました。この構造体は、前景色(黒、赤、緑など)やリセットなどの一般的なターミナルエスケープシーケンスを[]byte
スライスとして保持します。vt100EscapeCodes
というグローバル変数として、具体的なVT100エスケープシーケンスが定義され、Terminal
構造体のEscape
フィールドを通じてアクセスできるようになりました。これにより、アプリケーションはエスケープシーケンスを直接ハードコードする代わりに、t.Escape.Red
のようにシンボリックに参照できるようになります。
-
オートコンプリートコールバックの追加:
Terminal
構造体にAutoCompleteCallback func(line []byte, pos, key int) (newLine []byte, newPos int)
というフィールドが追加されました。- このコールバックは、キーが押されるたびに、現在の入力行、カーソル位置、押されたキーの情報を引数として呼び出されます。
- コールバックが
nil newLine
を返した場合、キー入力は通常通り処理されます。そうでない場合、コールバックが返したnewLine
とnewPos
が新しい入力行とカーソル位置として採用され、ターミナル表示が更新されます。これにより、アプリケーションは独自のオートコンプリートロジックを柔軟に実装できるようになりました。
-
長い入力行の表示問題の修正:
- 以前の
writeLine
関数は、ターミナルの幅を超えた場合に改行処理が不十分でした。 - このコミットでは、
writeLine
内のカーソル位置計算と改行ロジックが改善され、長い行がターミナル幅に合わせて正しく折り返されるようになりました。特に、t.cursorX == t.termWidth
のチェックとそれに続くt.cursorX = 0; t.cursorY++
の処理が、行の終端での自動改行を適切に処理するように修正されています。
- 以前の
-
Write()
メソッドの挙動改善:Terminal
構造体のWrite
メソッドが大幅に修正されました。以前は単に基盤となるio.ReadWriter
に直接書き込むだけでしたが、これによりユーザーの入力行が上書きされる問題がありました。- 新しい
Write
メソッドは、まずsync.Mutex
を使用してターミナルの状態をロックし、キー入力処理との競合を防ぎます。 - 次に、現在のカーソル位置から行の先頭まで戻り、現在の入力行とプロンプトをクリアします。
- その後、
Write
に渡されたbuf
の内容をターミナルに出力します。 - 最後に、プロンプトとユーザーの入力中の部分的な行を再描画し、カーソルを元の論理的な位置に戻します。この一連の処理により、非同期の出力があってもユーザーの入力が保護され、ターミナル表示の一貫性が保たれるようになりました。
-
ローカルエコーなしのプロンプトサポート (
ReadPassword
):Terminal
構造体にecho
というブール型のフィールドが追加され、ローカルエコーの有効/無効を制御できるようになりました。ReadPassword(prompt string)
という新しいメソッドが追加されました。このメソッドは、内部でt.echo = false
を設定し、readLine()
を呼び出すことで、入力文字が画面に表示されない状態でユーザーからの入力を受け付けます。読み取りが完了すると、echo
は元の状態に戻されます。これにより、パスワード入力などの機密性の高い操作が安全に行えるようになりました。
-
ターミナルサイズの取得 (
GetSize
):util.go
ファイルにGetSize(fd int) (width, height int, err error)
という新しい関数が追加されました。- この関数は、指定されたファイルディスクリプタ(
fd
)に対応するターミナルの幅と高さを取得します。 - 内部では、Unix系システムコールである
syscall.Syscall6
とsyscall.TIOCGWINSZ
(ターミナルウィンドウサイズを取得するためのioctl
コマンド)を使用して、ターミナルの寸法情報を取得します。これにより、アプリケーションは実行時にターミナルのサイズを動的に検出し、表示を調整できるようになります。
これらの変更は、exp/terminal
パッケージがより高度なCLIアプリケーションのニーズに応えられるようにするための重要なステップでした。
コアとなるコードの変更箇所
src/pkg/exp/terminal/terminal.go
// EscapeCodes contains escape sequences that can be written to the terminal in
// order to achieve different styles of text.
type EscapeCodes struct {
// Foreground colors
Black, Red, Green, Yellow, Blue, Magenta, Cyan, White []byte
// Reset all attributes
Reset []byte
}
var vt100EscapeCodes = EscapeCodes{
Black: []byte{keyEscape, '[', '3', '0', 'm'},
Red: []byte{keyEscape, '[', '3', '1', 'm'},
Green: []byte{keyEscape, '[', '3', '2', 'm'},
Yellow: []byte{keyEscape, '[', '3', '3', 'm'},
Blue: []byte{keyEscape, '[', '3', '4', 'm'},
Magenta: []byte{keyEscape, '[', '3', '5', 'm'},
Cyan: []byte{keyEscape, '[', '3', '6', 'm'},
White: []byte{keyEscape, '[', '3', '7', 'm'},
Reset: []byte{keyEscape, '[', '0', 'm'},
}
type Terminal struct {
// AutoCompleteCallback, if non-null, is called for each keypress
// with the full input line and the current position of the cursor.
// If it returns a nil newLine, the key press is processed normally.
// Otherwise it returns a replacement line and the new cursor position.
AutoCompleteCallback func(line []byte, pos, key int) (newLine []byte, newPos int)
// Escape contains a pointer to the escape codes for this terminal.
// It's always a valid pointer, although the escape codes themselves
// may be empty if the terminal doesn't support them.
Escape *EscapeCodes
// lock protects the terminal and the state in this object from
// concurrent processing of a key press and a Write() call.
lock sync.Mutex
c io.ReadWriter
prompt string
line []byte
// pos is the logical position of the cursor in line
pos int
// echo is true if local echo is enabled
echo bool
// ... (既存のフィールド)
}
func NewTerminal(c io.ReadWriter, prompt string) *Terminal {
return &Terminal{
Escape: &vt100EscapeCodes, // 新規追加
c: c,
prompt: prompt,
termWidth: 80,
termHeight: 24,
echo: true, // 新規追加
}
}
// handleKeyメソッド内のAutoCompleteCallbackの呼び出し部分
// ...
default:
if t.AutoCompleteCallback != nil {
t.lock.Unlock() // コールバック実行中はロックを一時解除
newLine, newPos := t.AutoCompleteCallback(t.line, t.pos, key)
t.lock.Lock() // コールバック終了後にロックを再取得
if newLine != nil {
if t.echo {
t.moveCursorToPos(0)
t.writeLine(newLine)
for i := len(newLine); i < len(t.line); i++ {
t.writeLine(space)
}
t.moveCursorToPos(newPos)
}
t.line = newLine
t.pos = newPos
return
}
}
// ...
// Writeメソッドの変更
func (t *Terminal) Write(buf []byte) (n int, err error) {
t.lock.Lock()
defer t.lock.Unlock()
if t.cursorX == 0 && t.cursorY == 0 {
// This is the easy case: there's nothing on the screen that we
// have to move out of the way.
return t.c.Write(buf)
}
// We have a prompt and possibly user input on the screen. We
// have to clear it first.
t.move(0, /* up */ 0, /* down */ t.cursorX, /* left */ 0 /* right */ )
t.cursorX = 0
t.clearLineToRight()
for t.cursorY > 0 {
t.move(1, /* up */ 0, 0, 0)
t.cursorY--
t.clearLineToRight()
}
if _, err = t.c.Write(t.outBuf); err != nil {
return
}
t.outBuf = t.outBuf[:0]
if n, err = t.c.Write(buf); err != nil {
return
}
t.queue([]byte(t.prompt))
chars := len(t.prompt)
if t.echo {
t.queue(t.line)
chars += len(t.line)
}
t.cursorX = chars % t.termWidth
t.cursorY = chars / t.termWidth
t.moveCursorToPos(t.pos)
if _, err = t.c.Write(t.outBuf); err != nil {
return
}
t.outBuf = t.outBuf[:0]
return
}
// ReadPasswordメソッドの追加
func (t *Terminal) ReadPassword(prompt string) (line string, err error) {
t.lock.Lock()
defer t.lock.Unlock()
oldPrompt := t.prompt
t.prompt = prompt
t.echo = false
line, err = t.readLine()
t.prompt = oldPrompt
t.echo = true
return
}
// readLineメソッドの追加 (ReadLineから分離)
func (t *Terminal) readLine() (line string, err error) {
// t.lock must be held at this point
// ... (既存のreadLineロジック)
}
src/pkg/exp/terminal/util.go
import "syscall" // 新規追加
import "unsafe" // 新規追加
// GetSize returns the dimensions of the given terminal.
func GetSize(fd int) (width, height int, err error) {
var dimensions [4]uint16
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0); err != 0 {
return -1, -1, err
}
return int(dimensions[1]), int(dimensions[0]), nil
}
コアとなるコードの解説
terminal.go
の変更点
-
EscapeCodes
とvt100EscapeCodes
:EscapeCodes
構造体は、ターミナルの色や属性を制御するためのバイトシーケンス(エスケープコード)をカプセル化します。これにより、アプリケーション開発者はマジックナンバーや複雑なエスケープシーケンスを直接扱う必要がなくなり、t.Escape.Red
のように可読性の高い方法でターミナルを操作できるようになります。vt100EscapeCodes
は、VT100互換ターミナルで一般的に使用される具体的なエスケープシーケンスを定義しています。NewTerminal
関数でTerminal
インスタンスが作成される際に、このvt100EscapeCodes
へのポインタがt.Escape
に設定されます。
-
Terminal
構造体へのフィールド追加:AutoCompleteCallback
: オートコンプリート機能の拡張ポイントを提供します。ユーザーがキーを押すたびにこのコールバックが呼び出され、アプリケーションは現在の入力状態に基づいて補完候補を生成したり、入力行を修正したりできます。これにより、exp/terminal
パッケージ自体がオートコンプリートのロジックを持つのではなく、アプリケーションがそのロジックを注入できるようになります。Escape
:EscapeCodes
構造体へのポインタを保持し、ターミナルのエスケープコードにアクセスするための統一されたインターフェースを提供します。lock sync.Mutex
:Terminal
構造体全体を保護するためのミューテックスです。Write()
メソッドとキー入力処理(handleKey
)が同時に実行されることによる競合状態を防ぎ、ターミナルの状態の一貫性を保ちます。特に、AutoCompleteCallback
の実行中はロックを一時的に解除し、コールバックがブロックされることを防ぎつつ、コールバック終了後に再度ロックを取得することで安全性を確保しています。echo bool
: ローカルエコーの有効/無効を制御します。ReadPassword
メソッドで一時的にfalse
に設定され、パスワード入力時に文字が表示されないようにします。
-
Write()
メソッドのロジック変更:- この変更は、ターミナルへの出力がユーザーの現在の入力行を「踏みつけない」ようにするためのものです。
t.lock.Lock()
とdefer t.lock.Unlock()
により、メソッド全体が排他的に実行されることが保証されます。- 出力を行う前に、現在のカーソル位置から行の先頭まで戻り、現在の入力行とプロンプトをクリアします。これは、
t.move
とt.clearLineToRight
を複数回呼び出すことで実現されます。 - その後、
buf
の内容をターミナルに書き込みます。 - 最後に、プロンプトとユーザーが入力中の部分的な行を再描画し、カーソルを元の論理的な位置に戻します。この複雑なシーケンスにより、非同期の出力があってもユーザーの入力が視覚的に保護され、スムーズなターミナルインタラクションが実現されます。
-
ReadPassword()
メソッドの追加:- このメソッドは、パスワード入力などの機密性の高い情報を扱うために導入されました。
- 内部で
t.echo
フィールドをfalse
に設定し、readLine()
(新しいプライベートメソッド)を呼び出します。これにより、ユーザーが入力した文字がターミナルに表示されなくなります。 - 入力が完了すると、
t.echo
は元のtrue
に戻され、通常のターミナル挙動に戻ります。
-
readLine()
の分離:- 既存の
ReadLine()
メソッドのコアロジックがreadLine()
というプライベートメソッドに分離されました。これは、ReadPassword()
がエコー制御のロジックを挟みつつ、同じ入力読み取りロジックを再利用できるようにするためです。
- 既存の
util.go
の変更点
GetSize()
関数の追加:- この関数は、Goアプリケーションが実行されているターミナルの現在の幅と高さを取得するためのものです。
syscall.Syscall6
を使用して、低レベルなioctl
システムコールを呼び出します。TIOCGWINSZ
は、ターミナルのウィンドウサイズを取得するための標準的なioctl
コマンドです。dimensions [4]uint16
という配列にターミナルの行数、列数、ピクセル幅、ピクセル高さが格納され、そこから幅と高さが抽出されて返されます。これにより、アプリケーションはターミナルのサイズ変更イベントに対応したり、動的に表示を調整したりすることが可能になります。
これらの変更は、exp/terminal
パッケージがより堅牢で、機能豊富で、ユーザーフレンドリーなCLIアプリケーションを構築するための強力な基盤となることを目指したものです。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語の実験的なパッケージに関する情報 (当時の状況):
exp
パッケージはGoの進化とともに変化しており、このコミット当時のexp/terminal
パッケージの直接的なドキュメントは現在では見つけにくい可能性があります。
参考にした情報源リンク
- コミットメッセージ自体
- Go言語のソースコード(
src/pkg/exp/terminal/terminal.go
およびsrc/pkg/exp/terminal/util.go
の変更履歴) - VT100エスケープシーケンスに関する一般的な知識
- Unix/Linuxにおける
ioctl
システムコールとTIOCGWINSZ
に関する一般的な知識 - Go言語の
sync
パッケージとミューテックスに関する一般的な知識 - Go言語の
syscall
パッケージに関する一般的な知識 - (注:
https://golang.org/cl/5479043
は、Goの内部的な変更レビューシステム(Gerrit)の古いリンク形式であり、現在のGoのバージョン管理システムでは直接アクセスできないか、異なる変更リストを指す可能性があります。そのため、このリンクからの直接的な情報は今回の解説には利用していません。)