Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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: shellterminalにリネームし、SetSizeを追加

これは実際にはシェルではなかったが、SSHのServerShellから名前が引き継がれていた。

2つの小さな機能変更:

  1. SetSizeを追加。これにより、例えばSSHメッセージに応答してターミナルのサイズを変更できるようになる。
  2. 現在の行にすでに何か入力がある場合、プロンプトを書き込まないようにする。

変更の背景

このコミットの主な背景は、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.Readerio.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つの側面に集約されます。

  1. 名称変更(リネーム):

    • ファイル名: src/pkg/exp/terminal/shell.gosrc/pkg/exp/terminal/terminal.go に、src/pkg/exp/terminal/shell_test.gosrc/pkg/exp/terminal/terminal_test.go に変更されました。
    • 型名: type Shell struct { ... }type Terminal struct { ... } に変更されました。
    • コンストラクタ関数名: NewShell(...)NewTerminal(...) に変更されました。
    • メソッドのレシーバ名: (ss *Shell)(t *Terminal) に変更されました。これは、Goの慣習として、構造体のレシーバ名は短くすることが推奨されるため、新しい型名Terminalの頭文字tが選ばれました。 これらの変更は、コードベース全体で一貫性を保ち、パッケージの意図をより正確に反映させるためのものです。
  2. SetSizeメソッドの追加:

    • func (t *Terminal) SetSize(width, height int) という新しいメソッドがTerminal構造体に追加されました。
    • このメソッドは、ターミナルの幅(termWidth)と高さ(termHeight)を更新します。これにより、外部からの情報(例: SSHクライアントからのウィンドウサイズ変更通知)に基づいて、ターミナルエミュレーションの内部状態を動的に調整することが可能になります。これは、特にリモートシェルやターミナルアプリケーションにおいて、クライアント側の表示とサーバー側の処理を同期させる上で不可欠な機能です。
  3. プロンプト表示ロジックの改善:

    • ReadLineメソッド内でプロンプトを書き込む前に、t.cursorX == 0という条件が追加されました。
    • 変更前: ss.writeLine([]byte(ss.prompt)) が常に実行されていた。
    • 変更後: if t.cursorX == 0 { t.writeLine([]byte(t.prompt)) ... }
    • この変更により、現在のカーソル位置が0(行の先頭)である場合にのみプロンプトが書き込まれるようになります。これは、ユーザーが既に入力中の行にいる場合(例えば、入力中に別のプロセスから出力があった場合など)に、プロンプトが不必要に再描画されたり、既存の入力と重なって表示されたりするのを防ぐためのものです。これにより、ユーザーエクスペリエンスが向上し、より自然なターミナル操作が可能になります。

これらの変更は、exp/terminalパッケージが提供するターミナルエミュレーション機能の正確性、柔軟性、およびユーザーフレンドリーさを向上させるものです。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は、以下のファイルと行に集中しています。

  1. 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.goterminal.goに更新され、ビルド対象のファイル名が変更されました。

  2. 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メソッドが追加されました。

  3. 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構造体の内部フィールドであるtermWidthtermHeightを更新します。

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