Go言語で標準出力を上書きする方法(プログレスバーなど)

コマンドラインツールを作るとき、進捗状況をリアルタイムで表示したい。

Ubuntu の例ではこういう感じ。

f:id:t-keita:20200922025024g:plain

ソースコード

上記の Go 言語でのサンプルコードは以下の通り。

タイマーとか使うのが面倒だったので、適当に長めのループを回して、全体の p % に達したタイミングで標準出力の表示を上書きしている。おまけに | 記号がグルグル回転する簡単なアニメーションも作ってみた。

package main

import "fmt"

var marks = []string{"|", "/", "-", "\\"}

func mark(i int) string {
    return marks[i%4]
}

func main() {
    cnt := 5000000000
    for i := 1; i <= cnt; i++ {
        if i%(cnt/100) == 0 {
            p := i / (cnt / 100)
            fmt.Printf("\rLoading: %s %2d%%", mark(p), p)
        }
    }
    fmt.Println("\nDone.")
}

ポイントは fmt.Printf("\rLoading: %s %2d%%", mark(p), p) という部分。

\r によって "キャリッジリターン" を出力している。

キャリッジリターン

キャリッジリターン (carriage return, CR) は制御文字の1つであり「カーソルを行頭に移動させる」という意味を持つ。Go 言語を含め、多くのプログラミング言語エスケープ文字 \r によってキャリッジリターンが表現される。

Carriage return - Wikipedia

標準出力とターミナルの挙動

標準出力 (stdout) はストリームである。ストリームにはデータが送られるのみで、すでに送ったデータが削除されることはない。ターミナル上の表示では文字データが上書きされているように見えるが、文字データの削除や更新が行われているわけではなく、ただ文字データが次々に送られているだけである。

今回の例では、Linux のターミナルが、キャリッジリターンを受け取ったときに、カーソルを行頭まで戻しているため、文字データが上書きされているように見えている。

実際、先ほどのプログラムの実行結果を log.txt にリダイレクトし、そのファイルの中身を xxd コマンドで見てみる。

$ go run main.go > log.txt
$ xxd log.txt | head
00000000: 0d4c 6f61 6469 6e67 3a20 2f20 2031 250d  .Loading: /  1%.
00000010: 4c6f 6164 696e 673a 202d 2020 3225 0d4c  Loading: -  2%.L
00000020: 6f61 6469 6e67 3a20 5c20 2033 250d 4c6f  oading: \  3%.Lo
00000030: 6164 696e 673a 207c 2020 3425 0d4c 6f61  ading: |  4%.Loa
00000040: 6469 6e67 3a20 2f20 2035 250d 4c6f 6164  ding: /  5%.Load
00000050: 696e 673a 202d 2020 3625 0d4c 6f61 6469  ing: -  6%.Loadi
00000060: 6e67 3a20 5c20 2037 250d 4c6f 6164 696e  ng: \  7%.Loadin
00000070: 673a 207c 2020 3825 0d4c 6f61 6469 6e67  g: |  8%.Loading
00000080: 3a20 2f20 2039 250d 4c6f 6164 696e 673a  : /  9%.Loading:
00000090: 202d 2031 3025 0d4c 6f61 6469 6e67 3a20   - 10%.Loading: 

標準出力に送られたすべての文字データがちゃんと残っていることが確認できる。 ちなみに、Unicode ではキャリッジリターンは 16 進数で 0d であり,この文字も一定間隔で出現していることが分かる。

まとめ

  • コマンドラインツールで出力を上書きするような表現は、標準出力のストリームにキャリッジリターンを送ることで実装できる。

  • 標準出力に指定されたターミナルは、キャリッジリターンを読んだとき、カーソルを行頭に移動させているだけであり、文字データの削除や更新を行っているわけではない。

  • なお、 標準出力にキャリッジリターンを送ることができるプログラミング言語であれば、同様のやり方で同様のふるまいを実装できるはず。