[インデックス 10345] ファイルの概要
このコミットは、Go言語のexp/terminal
パッケージにおける主要なリファクタリングと機能追加を目的としています。具体的には、パッケージ内の主要な型と関連するファイル名をshell
からterminal
へと変更し、よりその実態に即した名前に修正しています。また、ターミナルのサイズを動的に変更できるSetSize
メソッドが追加され、さらにプロンプトの表示ロジックが改善されています。
コミット
commit 252ef18d04a2560e66aef7b560bd02db92bed912
Author: Adam Langley <agl@golang.org>
Date: Fri Nov 11 14:04:33 2011 -0500
exp/terminal: rename shell to terminal and add SetSize
It was never really a shell, but the name carried
over from SSH's ServerShell.
Two small functional changes:
Add SetSize, which allows the size of the terminal
to be changed in response, say, to an SSH message.
Don't write the prompt if there's already something
on the current line.
R=rsc
CC=golang-dev
https://golang.org/cl/5376066
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/252ef18d04a2560e66aef7b560bd02db92bed912
元コミット内容
exp/terminal
: shell
をterminal
にリネームし、SetSize
を追加
これは実際にはシェルではなかったが、SSHのServerShell
から名前が引き継がれていた。
2つの小さな機能変更:
SetSize
を追加。これにより、例えばSSHメッセージに応答してターミナルのサイズを変更できるようになる。- 現在の行にすでに何か入力がある場合、プロンプトを書き込まないようにする。
変更の背景
このコミットの主な背景は、exp/terminal
パッケージ内の主要な構造体と関連ファイルの名称が、その実際の機能と乖離していた点にあります。元々shell
という名前が使われていましたが、コミットメッセージにあるように、これはSSHのServerShell
から引き継がれたものであり、このパッケージが提供する機能は厳密には「シェル」というよりも「ターミナル」の機能に近いものでした。
「シェル」という言葉は通常、ユーザーがコマンドを入力し、プログラムを実行するためのコマンドラインインターフェース(CLI)環境全体を指します。これには、コマンドの解釈、プロセスの管理、ファイルシステムの操作などが含まれます。しかし、このexp/terminal
パッケージは、VT100エミュレーションを通じて入出力の処理、カーソル移動、行編集などの低レベルなターミナル操作を提供することに特化しており、高レベルなシェル機能は含まれていませんでした。
そのため、より正確な名称であるterminal
への変更は、コードの意図を明確にし、将来的な誤解を防ぐための重要なリファクタリングです。
機能面では、SSHなどのリモート接続環境において、クライアント側からターミナルサイズ変更の通知(例: SIGWINCH
シグナルに相当するSSHのwindow-change
リクエスト)を受け取った際に、サーバー側のターミナルエミュレーションもそのサイズに合わせて調整する必要がありました。このニーズに応えるため、SetSize
メソッドが追加されました。
また、ユーザーエクスペリエンスの改善として、入力途中の行にプロンプトが重複して表示されるのを防ぐためのロジックが追加されました。これは、ユーザーが入力中に別の出力があった場合などに、プロンプトが不自然に再描画されるのを避けるためのものです。
前提知識の解説
VT100ターミナル
VT100は、1978年にDEC(Digital Equipment Corporation)が開発したビデオディスプレイターミナルです。これは、テキストベースのインターフェースでコンピュータと対話するための標準的な方法の一つとなり、そのエスケープシーケンス(特定の文字の並び)は、カーソル移動、文字の色変更、画面クリアなど、ターミナル画面を制御するためのデファクトスタンダードとなりました。現代の多くのターミナルエミュレータ(例えば、Linuxのxterm
やmacOSのTerminal.app
、Windowsのcmd.exe
やPowerShellなど)は、VT100の機能をエミュレートしており、これによりプログラムはプラットフォームに依存せずターミナルを操作できます。
io.ReadWriter
インターフェース
Go言語のio
パッケージは、基本的なI/Oプリミティブを提供します。io.ReadWriter
インターフェースは、io.Reader
とio.Writer
の両方のインターフェースを組み合わせたものです。
io.Reader
インターフェース:Read(p []byte) (n int, err error)
メソッドを持ち、データソースからバイトを読み込む機能を提供します。io.Writer
インターフェース:Write(p []byte) (n int, err error)
メソッドを持ち、データシンクにバイトを書き込む機能を提供します。
exp/terminal
パッケージでは、このio.ReadWriter
インターフェースを介して、実際のターミナルデバイス(標準入力/出力など)との間でデータのやり取りを行います。これにより、具体的なターミナル実装に依存せず、抽象化されたI/O操作が可能になります。
exp/terminal
パッケージ
Go言語のexp
(experimental)リポジトリは、まだ標準ライブラリに組み込まれていない、実験的なパッケージや機能を含む場所です。exp/terminal
パッケージは、GoプログラムがVT100互換のターミナルと対話するための低レベルな機能を提供します。これには、行編集、カーソル制御、キー入力の処理などが含まれます。このパッケージは、インタラクティブなCLIアプリケーションや、SSHサーバーのようなターミナルエミュレーションを必要とするアプリケーションの構築に利用されます。
SSHのServerShell
SSH(Secure Shell)プロトコルは、セキュアなリモートアクセスを提供します。SSHセッションが確立されると、クライアントはサーバーに対して様々なリクエストを送信できます。その一つに「シェルリクエスト」があります。これは、サーバー上でシェルプロセス(例: Bash, Zsh)を起動し、その標準入出力とエラー出力をクライアントのターミナルに接続するものです。
SSHプロトコルには、クライアントがサーバーにターミナルサイズ変更を通知するためのメカニズムも含まれています。これは通常、クライアント側のターミナルサイズが変更された際に、サーバー側の擬似ターミナル(pty)のサイズもそれに合わせて更新するために使用されます。このコミットで追加されたSetSize
メソッドは、このようなSSHのwindow-change
メッセージなどに応答して、Goのexp/terminal
パッケージが管理するターミナルエミュレーションの内部状態を更新するために利用されます。
技術的詳細
このコミットにおける技術的な変更点は、主に以下の3つの側面に集約されます。
-
名称変更(リネーム):
- ファイル名:
src/pkg/exp/terminal/shell.go
がsrc/pkg/exp/terminal/terminal.go
に、src/pkg/exp/terminal/shell_test.go
がsrc/pkg/exp/terminal/terminal_test.go
に変更されました。 - 型名:
type Shell struct { ... }
がtype Terminal struct { ... }
に変更されました。 - コンストラクタ関数名:
NewShell(...)
がNewTerminal(...)
に変更されました。 - メソッドのレシーバ名:
(ss *Shell)
が(t *Terminal)
に変更されました。これは、Goの慣習として、構造体のレシーバ名は短くすることが推奨されるため、新しい型名Terminal
の頭文字t
が選ばれました。 これらの変更は、コードベース全体で一貫性を保ち、パッケージの意図をより正確に反映させるためのものです。
- ファイル名:
-
SetSize
メソッドの追加:func (t *Terminal) SetSize(width, height int)
という新しいメソッドがTerminal
構造体に追加されました。- このメソッドは、ターミナルの幅(
termWidth
)と高さ(termHeight
)を更新します。これにより、外部からの情報(例: SSHクライアントからのウィンドウサイズ変更通知)に基づいて、ターミナルエミュレーションの内部状態を動的に調整することが可能になります。これは、特にリモートシェルやターミナルアプリケーションにおいて、クライアント側の表示とサーバー側の処理を同期させる上で不可欠な機能です。
-
プロンプト表示ロジックの改善:
ReadLine
メソッド内でプロンプトを書き込む前に、t.cursorX == 0
という条件が追加されました。- 変更前:
ss.writeLine([]byte(ss.prompt))
が常に実行されていた。 - 変更後:
if t.cursorX == 0 { t.writeLine([]byte(t.prompt)) ... }
- この変更により、現在のカーソル位置が0(行の先頭)である場合にのみプロンプトが書き込まれるようになります。これは、ユーザーが既に入力中の行にいる場合(例えば、入力中に別のプロセスから出力があった場合など)に、プロンプトが不必要に再描画されたり、既存の入力と重なって表示されたりするのを防ぐためのものです。これにより、ユーザーエクスペリエンスが向上し、より自然なターミナル操作が可能になります。
これらの変更は、exp/terminal
パッケージが提供するターミナルエミュレーション機能の正確性、柔軟性、およびユーザーフレンドリーさを向上させるものです。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、以下のファイルと行に集中しています。
-
src/pkg/exp/terminal/Makefile
:--- a/src/pkg/exp/terminal/Makefile +++ b/src/pkg/exp/terminal/Makefile @@ -6,7 +6,7 @@ include ../../../Make.inc TARG=exp/terminal GOFILES=\ - shell.go\ + terminal.go\ ifneq ($(GOOS),windows) GOFILES+=util.go
GOFILES
変数内のshell.go
がterminal.go
に更新され、ビルド対象のファイル名が変更されました。 -
src/pkg/exp/terminal/{shell.go => terminal.go}
: ファイル名がshell.go
からterminal.go
に変更され、内部の型名、関数名、レシーバ名がShell
からTerminal
に一括で変更されています。-
型定義の変更:
--- a/src/pkg/exp/terminal/shell.go +++ b/src/pkg/exp/terminal/terminal.go @@ -6,9 +6,9 @@ package terminal import "io" -// Shell contains the state for running a VT100 terminal that is capable of +// Terminal contains the state for running a VT100 terminal that is capable of // reading lines of input. -type Shell struct { +type Terminal struct { c io.ReadWriter prompt string
Shell
構造体がTerminal
構造体に変更されました。 -
コンストラクタ関数の変更:
--- a/src/pkg/exp/terminal/shell.go +++ b/src/pkg/exp/terminal/terminal.go @@ -34,12 +34,12 @@ type Shell struct { inBuf [256]byte }\ -// NewShell runs a VT100 terminal on the given ReadWriter. If the ReadWriter is +// NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is // a local terminal, that terminal must first have been put into raw mode.\ // prompt is a string that is written at the start of each input line (i.e.\ // "> "). -func NewShell(c io.ReadWriter, prompt string) *Shell { - return &Shell{ +func NewTerminal(c io.ReadWriter, prompt string) *Terminal { + return &Terminal{ \tc: c, \tprompt: prompt, \ttermWidth: 80,
NewShell
関数がNewTerminal
関数に変更されました。 -
メソッドレシーバの変更:
queue
,moveCursorToPos
,handleKey
,writeLine
,Write
,ReadLine
といった既存のメソッドのレシーバが*Shell
から*Terminal
に変更されています。例えば、func (ss *Shell) queue(data []byte)
はfunc (t *Terminal) queue(data []byte)
に変更されています。 -
ReadLine
メソッド内のプロンプト表示ロジックの変更:--- a/src/pkg/exp/terminal/shell.go +++ b/src/pkg/exp/terminal/terminal.go @@ -290,10 +290,12 @@ func (ss *Shell) Write(buf []byte) (n int, err error) { } // ReadLine returns a line of input from the terminal. -func (ss *Shell) ReadLine() (line string, err error) { -\tss.writeLine([]byte(ss.prompt))\ -\tss.c.Write(ss.outBuf)\ -\tss.outBuf = ss.outBuf[:0]\ +func (t *Terminal) ReadLine() (line string, err error) { +\tif t.cursorX == 0 { +\t\tt.writeLine([]byte(t.prompt))\ +\t\tt.c.Write(t.outBuf)\ +\t\tt.outBuf = t.outBuf[:0]\ +\t} for { -\t\t// ss.remainder is a slice at the beginning of ss.inBuf +\t\t// t.remainder is a slice at the beginning of t.inBuf \t// containing a partial key sequence -\t\treadBuf := ss.inBuf[len(ss.remainder):] +\t\treadBuf := t.inBuf[len(t.remainder):] \tvar n int -\t\tn, err = ss.c.Read(readBuf) +\t\tn, err = t.c.Read(readBuf) \tif err != nil { \t\treturn \t} @@ -301,16 +303,16 @@ func (ss *Shell) ReadLine() (line string, err error) { \tif err == nil { -\t\t\tss.remainder = ss.inBuf[:n+len(ss.remainder)] -\t\t\trest := ss.remainder +\t\t\tt.remainder = t.inBuf[:n+len(t.remainder)] +\t\t\trest := t.remainder \t\tlineOk := false \t\tfor !lineOk { \t\t\tvar key int @@ -336,16 +338,16 @@ func (ss *Shell) ReadLine() (line string, err error) { \t\t\tif key == keyCtrlD { \t\t\t\treturn "", io.EOF \t\t\t} -\t\t\t\tline, lineOk = ss.handleKey(key) +\t\t\t\tline, lineOk = t.handleKey(key) \t\t}\ \t\tif len(rest) > 0 { -\t\t\t\tn := copy(ss.inBuf[:], rest) -\t\t\t\tss.remainder = ss.inBuf[:n] +\t\t\t\tn := copy(t.inBuf[:], rest) +\t\t\t\tt.remainder = t.inBuf[:n] \t\t} else { -\t\t\t\tss.remainder = nil +\t\t\t\tt.remainder = nil \t\t} -\t\t\tss.c.Write(ss.outBuf) -\t\t\tss.outBuf = ss.outBuf[:0] +\t\t\tt.c.Write(t.outBuf) +\t\t\tt.outBuf = t.outBuf[:0] \t\tif lineOk { \t\t\treturn \t\t}
ReadLine
メソッドの冒頭にif t.cursorX == 0
の条件が追加されました。 -
SetSize
メソッドの追加:--- a/src/pkg/exp/terminal/shell.go +++ b/src/pkg/exp/terminal/terminal.go @@ -354,3 +356,7 @@ func (ss *Shell) ReadLine() (line string, err error) { } panic("unreachable") }\ +\ +func (t *Terminal) SetSize(width, height int) { +\tt.termWidth, t.termHeight = width, height +}\
SetSize
メソッドが追加されました。
-
-
src/pkg/exp/terminal/{shell_test.go => terminal_test.go}
: ファイル名がshell_test.go
からterminal_test.go
に変更され、テストコード内のNewShell
の呼び出しがNewTerminal
に更新されています。--- a/src/pkg/exp/terminal/shell_test.go +++ b/src/pkg/exp/terminal/terminal_test.go @@ -41,7 +41,7 @@ func (c *MockTerminal) Write(data []byte) (n int, err error) { func TestClose(t *testing.T) { c := &MockTerminal{} - ss := NewShell(c, "> ") + ss := NewTerminal(c, "> ") line, err := ss.ReadLine() if line != "" { t.Errorf("Expected empty line but got: %s", line) @@ -95,7 +95,7 @@ func TestKeyPresses(t *testing.T) { toSend: []byte(test.in), bytesPerRead: j, } - ss := NewShell(c, "> ") + ss := NewTerminal(c, "> ") line, err := ss.ReadLine() if line != test.line { t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line)
コアとなるコードの解説
Terminal
構造体と名称変更
以前のShell
構造体は、VT100ターミナルをエミュレートし、行入力の読み取りを行うための状態を保持していました。このコミットでは、その名前がTerminal
に変更されました。これは、この構造体が提供する機能が、一般的な「シェル」の機能(コマンドの解釈や実行など)ではなく、より低レベルな「ターミナル」の入出力制御、カーソル管理、行編集に特化していることを明確にするためです。
// Terminal contains the state for running a VT100 terminal that is capable of
// reading lines of input.
type Terminal struct {
c io.ReadWriter // 実際のターミナルデバイスとのI/Oを行う
prompt string // プロンプト文字列
// ... その他のフィールド(カーソル位置、バッファなど)
}
// NewTerminal runs a VT100 terminal on the given ReadWriter.
// ...
func NewTerminal(c io.ReadWriter, prompt string) *Terminal {
return &Terminal{
c: c,
prompt: prompt,
termWidth: 80, // デフォルトのターミナル幅
termHeight: 24, // デフォルトのターミナル高さ
// ...
}
}
NewShell
からNewTerminal
への変更も同様に、この構造体の役割をより正確に反映しています。
SetSize
メソッドの追加
SetSize
メソッドは、Terminal
構造体に新しい機能を追加します。このメソッドは、ターミナルの幅と高さを引数として受け取り、Terminal
構造体の内部フィールドであるtermWidth
とtermHeight
を更新します。
func (t *Terminal) SetSize(width, height int) {
t.termWidth, t.termHeight = width, height
}
この機能は、特にリモート接続環境(例: SSH)で重要です。SSHクライアントがウィンドウサイズを変更した場合、サーバー側の擬似ターミナル(pty)のサイズもそれに合わせて変更される必要があります。このSetSize
メソッドは、そのような外部からのサイズ変更イベントをexp/terminal
パッケージが処理できるようにするためのインターフェースを提供します。これにより、ターミナルエミュレーションが常に実際の表示サイズと同期し、行の折り返しやカーソル位置の計算が正しく行われるようになります。
ReadLine
メソッド内のプロンプト表示ロジックの改善
ReadLine
メソッドは、ユーザーからの1行の入力を読み取るための主要なメソッドです。このコミットでは、プロンプトの表示方法に小さな改善が加えられました。
変更前は、ReadLine
が呼び出されるたびに無条件でプロンプトが書き込まれていました。しかし、これにより、ユーザーが既に入力中の行にいる場合(例えば、非同期で別の出力がターミナルに書き込まれた後など)に、プロンプトが既存の入力の上に重複して表示される可能性がありました。
変更後は、プロンプトを書き込む前にif t.cursorX == 0
という条件が追加されました。
func (t *Terminal) ReadLine() (line string, err error) {
if t.cursorX == 0 { // カーソルが行の先頭にある場合のみプロンプトを書き込む
t.writeLine([]byte(t.prompt))
t.c.Write(t.outBuf)
t.outBuf = t.outBuf[:0]
}
for {
// ... キー入力処理
}
}
t.cursorX
は、現在のカーソルがターミナル行のどの列にあるかを示す0ベースのインデックスです。この条件により、カーソルが行の先頭(列0)にある場合にのみプロンプトが書き込まれるようになります。これにより、ユーザーが既に入力中の行にいる場合でも、プロンプトが不必要に再描画されたり、既存の入力と重なったりすることがなくなり、よりスムーズで直感的なユーザーエクスペリエンスが提供されます。
関連リンク
- Go CL 5376066: https://golang.org/cl/5376066
参考にした情報源リンク
- VT100 - Wikipedia: https://ja.wikipedia.org/wiki/VT100
- Go言語のioパッケージ: https://pkg.go.dev/io
- Go言語のexpリポジトリ: https://pkg.go.dev/golang.org/x/exp
- SSHプロトコル (RFC 4254 - The Secure Shell (SSH) Connection Protocol): https://www.rfc-editor.org/rfc/rfc4254#section-6.2
- Go言語のレシーバ名に関する慣習: https://go.dev/doc/effective_go#receivers