2013-12-20

ターミナルウィンドウに雪を降らせよう! - Bash Advent Calendar - Day 13

いまは、ぼくのこころの中では 12 月 13 日の 210時ぐらいです。もはや Advent Calendar の体をなしてない気がしますが、細かいことは気にしないで行きましょう。

terminfo

端末エミューレーター(以下「ターミナル」)は色々と標準、デファクトスタンダードが存在します。これから使う ANSI エスケープシーケンスについては、それと同等の機能のあるターミナルであれば、まずもって ANSI エスケープシーケンスに対応しています。

が、ターミナルの機能は ANSI エスケープシーケンスでサポートされているものだけではなく色々あるので、そうした色々な機能を抽象化して定義したのが terminfo です。昔は termcap が主流でしたが、最近は terminfo の方が Linux 界隈では優勢だと思います。

tputコマンド

tputコマンドは、terminfoで定義されている機能名を引数にとり、使用しているターミナルに対応するコマンドを出力します。

たとえば、画面クリア "clear" では
clear="$(tput clear)"
printf "%q\n" "$clear"
$'\E[H\E[2J'
となります。ちなみに、printf %q とか $'...' 記法に「Bashらしさ」があるので、それで記事が書けそうな気がしてきました。

乱数

$RANDOM を参照すると Bash は擬似乱数を返してくれます。が、値は[0, 32767]の範囲とかなりしょぼい仕様です。以下では、頑張って[0, ターミナルカラム数)で一様に分布するっぽい乱数を生成していますが、実用上、Linux であれば
rand() {
  local -i r="0x$(
    dd if=/dev/urandom bs=4 count=1 2>/dev/null |
    od -A n -t x4 |
    tr -d ' ')"
  echo $((r % $1))
}
でいいでしょう。64bit 環境であれば dd と od の引数に出てくる 4 は 8 に変えてください。

さて、[0, 32767] の範囲でしか値を返さない乱数の場合、そこから [0, x) の範囲で得ようとした場合、単純に (mod x) を取ると偏りが出てしまいます。そこで以下では、例えば x が 80 であれば
32767 % 80 = 47
32767 - 47 = 32720
ということで、得られた乱数が [0, 32720) の範囲にある場合だけ有効な乱数とみなすことで一様な乱数を得ています。

雪を降らせてみよう

元々この記事は「ターミナルウィンドウに雪を降らせよう!」に触発されて書いてます。というかロジックと変数名は(あえて)そのままです。
C=$(tput cols)
RAND_MAX=32767;
rand(){
  local RAND_MAX=$(( RAND_MAX - (RAND_MAX + 1) % $1 ))
  local r=$((RAND_MAX+1))
  while ((r > RAND_MAX)); do
    r=$RANDOM
  done
  echo $((r % $1))
}
declare -i -a a=()
tput clear
tput home
while :; do
  a[$(rand $C)]=0
  for x in ${!a[@]}; do
    o=${a[x]}
    a[x]+=1
    printf "%s %s\u2743%s" $(tput cup $o $x) \
            $(tput cup ${a[x]} ${x}) $(tput cup 0 0)
  done
  usleep 0.1
done
元ネタコードと違うところは
  • ターミナルのカラム数の取得は stty ではなく tput コマンドを使う
  • randしょぼい
  • ANSI エスケープシーケンスは使わずに tput コマンドを使う
  • Bash なら \uxxxx で Unicode 文字の表示がそのまま可能
というところでしょうか。それ以外で使ってる Bash 機能は今まですでに説明したと思います。

tput に関しては、tput -S で標準入力からコマンドシーケンスを受け取れるらしいので、それを使うともっと短く書けるかもしれません。

0 件のコメント:

prometheusのrate()関数の罠

 久しぶりのAdventカレンダー挑戦、うまくいく気がしません。 閑話休題。実のところ、rate()関数というよりは、サーバー側のmetric初期化問題です。 さて、何らかのサーバーAがあったとして、それが更に他のサーバーBにRPCを送っているとします。サーバーBの方でホワイトボ...