LoginSignup
7
8

More than 3 years have passed since last update.

[Go言語]ターミナルの入力をリアルタイムに取得する方法

Last updated at Posted at 2020-05-14

はじめに

本記事では、Go言語を使用してターミナルの入力をリアルタイムで取得する方法をご紹介します。
今回ご紹介するのは、UNIX系OSに限った話です。
Windowsの場合は、Microsoftが用意してるAPIで実現できるらしいが......(調査不足)

目次

結論

ターミナルを非カノニカルモードにすること、即時に入力を取得可能です。
非カノニカルモードにするには、termios構造体のフィールドの値を変更しなければなりません。
以下では、サンプルコードとLinuxのマニュアルを交えて、それぞれのモードを説明します。

動作環境

  • macOS Catalina
  • CentOS 7
  • go version go1.12.6

カノニカルモードとは

非カノニカルモードを説明する前に、まずカノニカルモードについてです。
わかりやすく言えば、デフォルトの状態ですね。Enterが押下されるまで入力を受け付けます。

引用 Linuxの日本語翻訳マニュアル

  • 入力は行単位に行われる。 行区切り文字が打ち込まれた時点で、入力行が利用可能となる。行区切り文字は NL, EOL, EOL2 および行頭での EOF である。 EOF 以外の場合、 read(2) が返すバッファーに行区切り文字も含められる

  • 行編集が有効となる (ERASE, KILL が効果を持つ。 IEXTEN フラグが設定された場合は、 WERASE, REPRINT, LNEXT も効果を持つ)。 read(2) は最大でも 1行の入力しか返さない。 read(2) が要求したバイト数が現在の入力行のバイト数よりも少ない場合、 要求したのと同じバイト数だけが読み込まれ、 残りの文字は次回の read(2) で読み込まれる

非カノニカルモードとは

非カノニカルモード、リアルタイムに入力を受け付け可能なのがこちらのモードです。
MIN (c_cc[VMIN])で受付文字数を設定できます。
例えば、c_cc[VMIN] = 3とした場合、3文字入力した時点で、入力完了となりプログラムに読み込まれます。
TIME (c_cc[VTIME])でタイムアウト時間を設定でき、タイムアウトを必要としない場合は0にします。

引用 Linuxの日本語翻訳マニュアル

  • 非カノニカルモードでは、入力は即座に利用可能となり (ユーザーは行区切り文字を打ち込む必要はない)、入力処理は実行されず、行編集は無効となる。 MIN (c_cc[VMIN]) と TIME (c_cc[VTIME]) の設定により、 read(2) が完了する条件が決定される

サンプルコード

C言語の場合、termios構造体にアクセスする際には、「termios.h」の関数群を使用します。
これをGo言語で同じように使用できるよう、有志が作成したpkg/termパッケージをお借りします。
termios構造体のパラメータを変更するときは、Tcgetattr()で現在の設定を取得してから、ビット演算などで必要な設定をしていきます。
設定できるパラメータは、Linuxの日本語翻訳マニュアルを参考にしてください。
ちなみに、C言語で使用できるフラグは、基本的にはpkg/termパッケージでも使用できました。
必要な設定が完了したら、Tcsetattr()で反映できます。

sample.go
package main

import (
    "fmt"
    "syscall"

    "github.com/pkg/term/termios"
)

func main() {
    var ttystate syscall.Termios

    // ターミナルの設定を取得
    termios.Tcgetattr(uintptr(syscall.Stdin), &ttystate)

    // ターミナルの設定変更
    setNonCanonicalMode(&ttystate)

    // 標準入力を取得
    bufCh := make(chan []byte, 1)
    go readBuffer(bufCh)

    for {
        fmt.Printf(" あなたが入力したのは: %c\n", <-bufCh)
    }
}

// 非カノニカルモードに設定する
func setNonCanonicalMode(attr *syscall.Termios) {

    // カノニカルモードを無効に設定 (&^ AND NOT)
    attr.Lflag &^= syscall.ICANON

    // 読み込み時の最小文字数 = 1文字
    attr.Cc[syscall.VMIN] = 1

    //非カノニカル読み込み時のタイムアウト時間 = 0
    attr.Cc[syscall.VTIME] = 0

    // 変更した設定を反映
    termios.Tcsetattr(uintptr(syscall.Stdin), termios.TCSANOW, attr)
}

// バッファの値を取得する
func readBuffer(bufCh chan []byte) {
    buf := make([]byte, 1024)

    for {
        if n, err := syscall.Read(syscall.Stdin, buf); err == nil {
            bufCh <- buf[:n]
        }
    }
}

上記コードを実行すると、以下のような挙動になります。
入力したキーが画面に表示された後に、Printfが実行されます。
ちなみにVMINの数値を増やせば、複数の値を取得可能です。

Demo01

Rawモードとは

非カノニカルモードの特徴に加えて、入力したキーが画面に表示されません。
また特殊処理が効かないので、Ctrl + C で強制終了ができなくなります。

引用 Linuxの日本語翻訳マニュアル

  • 入力は文字単位に可能であり、エコーが無効となり、 端末の入出力文字に対する特殊処理はすべて無効となる

サンプルコード

Linuxの日本語翻訳マニュアルに示す通りにRawモードに設定したパターンです。(※ 不必要だと判断した箇所はコメントアウトしてます。)

下記コードのように何も考えずにマニュアル通りに設定してもいいが、必要に応じて設定パラメータを選択した方がいい気がしますね。

sample.go
package main

import (
    "fmt"
    "syscall"

    "github.com/pkg/term/termios"
)

func main() {
    var ttystate syscall.Termios

    // ターミナルの設定を取得
    termios.Tcgetattr(uintptr(syscall.Stdin), &ttystate)

    // ターミナルの設定変更 ※ここを変更
    //setNonCanonicalMode(&ttystate)
    setRawMode(&ttystate)

    bufCh := make(chan []byte, 1)
    go readBuffer(bufCh)

    for {
        fmt.Printf(" あなたが入力したのは: %c\n", <-bufCh)
    }
}

// Rawモードに設定する
func setRawMode(attr *syscall.Termios) {

    // 設定値は、[https://linuxjm.osdn.jp/html/LDP_man-pages/man3/termios.3.html]から引用
    // OPOSTをビットクリアすると、自分の環境では出力がおかしくなるのでコメントアウト
    // ISIGを無効にすると、Ctrl+Cが押せなくてデバッグがしづらいのでコメントアウト
    // マニュアルでは、文字サイズをCS8に指定しているが、今回の検証では不必要と判断してコメントアウト

    attr.Iflag &^= syscall.BRKINT | syscall.ICRNL | syscall.INPCK | syscall.ISTRIP | syscall.IXON

    //attr.Oflag &^= syscall.OPOST

    attr.Cflag &^= syscall.CSIZE | syscall.PARENB

    //attr.Cflag |= syscall.CS8

    attr.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.IEXTEN //| syscall.ISIG

    attr.Cc[syscall.VMIN] = 1
    attr.Cc[syscall.VTIME] = 0

    // 変更した設定を反映
    termios.Tcsetattr(uintptr(syscall.Stdin), termios.TCSANOW, attr)
}

// バッファの値を取得する
func readBuffer(bufCh chan []byte) {
    // 省略
}

Demo02
上記の非カノニカルモードと違って、押下したキーが画面に表示されないですね。

参考サイト

https://linuxjm.osdn.jp/html/LDP_man-pages/man3/termios.3.html
https://www.slideshare.net/c-bata/golang-ui
https://grimoire.f5.si/archives/125

おわりに

termiosについてC言語で扱う情報は、たくさんあるのですが、Go言語でやってる事例が少なかったです。
ジョークコマンドを作成するために調べ始めたのですが、思ったより時間を取られてしまいました。
未来の自分のため、そして同じように詰まってる人のために記事にまとめました。
普段意識しない低レイヤーに触れることができて、面白かったですね。

7
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8