はじめに
開発に行き詰まって、ビールを飲みたくなった経験はありませんか?
でも仕事中だし...
今回は、仕事中でもビールを飲んでる気分が味わえる、「ターミナルビール」をshell scriptで作ってみたので、作り方を共有したいと思います。
何を作ったのか
今回は、ターミナルで動かしたいということもあって、Shell Scriptオンリーで実装しています。
Shell Scriptの基礎
echoもしくはprintfで描画しています。
すなわち、文字列を工夫することでビールを表現しているのです。
文字列を制御する上で必要になる知識は以下です。
文字列のカラー
例えば、echoで黄色のHello Worldを打ちたかったら、
echo -e "\033[33mHello World\033[0m"
と記述します。

すると、ご覧の通り、黄色いHello Worldがターミナルで表示されます。
Hello Worldの前についている
\033[33mが黄色を表すコマンド、
Hello Worldの後についている
\033[0mが書体をリセットするコマンドです。
今回用いたカラーは以下のとおりです。
| カラー | コマンド |
|---|---|
| \033[33m | 黄色 |
| \033[34m | 青色 |
| \033[37m | 白色 |
初見殺しですが、もう少し踏ん張ってください!
カーソル移動
文字列を出力する際、カーソルの制御ができます。例えば、
echo -e "Hello\033[1BWorld"
と打つと、

Helloの後、カーソルが下に移動して、Worldが描画されています。
\033[1B
がカーソルを下にするコマンドです。
カーソル移動をまとめると、
| カーソル移動 | コマンド |
|---|---|
| \033[1A | 上 |
| \033[1B | 下 |
| \033[1C | 右 |
| \033[1D | 左 |
| となります。 |
これを駆使すると

このような段々の描画もできるようになります。
@sudo00 さんのこちらの記事に詳しくまとまっています。今回も参考にさせていただきました。
また、カーソル移動は
tput cup X座標 Y座標
でも可能です。
今回は、大まかな位置をtput cupで指定し、相対移動を前述のコマンドで行いました。
変数定義
これまで、カラーとカーソル移動の説明をしました。しかしながら、毎回\033[1Aとか打つのはめんどくさいですよね。
ですので、これらを変数として定義します。
readonly YELLOW="\033[33m" #黄色
readonly BLUE="\033[34m" #黄色
readonly WHITE="\033[37m" #白色
readonly RESET="\033[0m" #リセット
readonly UP="\033[1A" #カーソル上
readonly DOWN="\033[1B" #カーソル下
readonly RIGHT="\033[1C" #カーソル右
readonly LEFT="\033[1D" #カーソル左
readonlyは、読み込み専用を表します。
変数定義の際の=の前後はスペースを入れないように!
変数を定義することで、
echo -e "${YELLOW}Hello World${RESET}"
このようにして、黄色いHello Worldが表現できることになります。
変数は${変数}で呼び出すことが可能です。
随分見通しが良くなりましたね。
ターミナルの縦横サイズ
tput linesで縦幅、tput colsで横幅が取得できます。

この場合だと縦30行、横78列ですね。
計算
shell scriptでの計算は少し厄介です。
$((1 + 1))
のように、$(())で計算式をくくることで計算できます。
また、小数が扱えませんので、すべて整数で表現しないといけないことになります。

もし小数を扱いたい場合は、bcコマンドを使うことで計算できます。
echo "1.2+1" | bc
こんな感じ。
小数が使えないので、計算の順番も工夫しなければなりません。
例えば、
echo "$((10 / 100 * 500))"
は、左から計算されますので、10 / 100 = 0、0 * 500 = 0となってしまいます。
これを防ぐために掛け算を先に持ってきます。
echo "$((500 * 100 / 10))"
描画
Shell Scriptの基礎知識を学んだところで、実際に描画していきます。
変数定義
描画をしていく前に、変数を定義していきます。上でも述べましたが、色、カーソルの他に、注ぐ時間やジョッキの高さ&横幅も定義しています。
# !/bin/bash
readonly H_TER=$(tput lines) #ターミナルの縦幅
readonly W_TER=$(tput cols) #ターミナルの横幅
w1=$((W_TER * 30 / 100)) #計算用w
readonly MIN_W_MUG=$((w1 % 2 == 0 ? w1 : w1 + 1)) #ビールジョッキの最大の横幅(形は後で変える)必ず偶数
readonly H_MUG=$((H_TER * 40 / 100)) #ビールジョッキの縦幅
readonly Y_BOTTOM=$((H_TER - 5)) #ジョッキの底の位置
readonly POUR_SPEED=500 #注ぐ速度
readonly MOVE_SPEED=$((MIN_W_MUG / 3 * 2)) #ビールジョッキ登場スピード
readonly YELLOW="\033[33m" #黄色
readonly BLUE="\033[34m" #黄色
readonly WHITE="\033[37m" #白色
readonly RESET="\033[0m" #リセット
readonly UP="\033[1A" #カーソル上
readonly DOWN="\033[1B" #カーソル下
readonly RIGHT="\033[1C" #カーソル右
readonly LEFT="\033[1D" #カーソル左
ビールジョッキの横幅
ビールジョッキの横幅は、高さが高くなるにつれ大きくしたいので、
function get_beer_w() {
local h=$1 #ある高さ
local w=$((h * 60 / 100 + MIN_W_MUG)) #その時の横幅
if ((w % 2 == 1)); then
#奇数だとズレちゃう
w=$((w + 1))
fi
echo "$w"
}
このような関数としました。
※横幅を偶数にしていますが、その理由は

奇数だとこんな感じで横幅が対称とならないためです。
- 引数は
$1,$2,.と受け取れます。 -
get_beer_w 引数1 引数2という感じで引数を渡します。 - 戻り値は、
echo 戻り値で返せます。
各行の横幅は、
WS_MUG=()
for ((i = 0; i < H_MUG; i++)); do
WS_MUG[i]=$(get_beer_w "$i")
done
こんな感じで配列にしておきます。
WS_MUGには20,22,22,24,26,・・・
という値が入っていくことになります。
描画の流れ
描画の流れは以下のとおりです。
- ビールジョッキが横から登場
- 注ぎ口の描画
- ビールが注がれる演出
- 溜まってくビールの描画
ビールジョッキが横から登場
ビールの外形は以下のコードで実現しています。
MUG_LINES=()
W_BASE=$((H_MUG * 60 / 100 + MIN_W_MUG)) # 1番広い幅
if ((W_BASE % 2 == 1)); then
W_BASE=$((W_BASE + 1))
fi
for ((i = 0; i < H_MUG; i++)); do
w_current=${WS_MUG[i]}
indent=$(((W_BASE - w_current) / 2))
line=$(printf "%${indent}s" "") #左インデント
if ((i == 0)); then
line+=$(printf "%${w_current}s" "" | tr ' ' '█')
else
inner_w=$((w_current - 2))
line+="█"$(printf "%${inner_w}s" "")"█"
fi
MUG_LINES[$i]="$line"
done
-
indent=$(((W_BASE - w_current) / 2))で左側の空白(indent)の数を出し、カップが真ん中に来るようにしています。 -
printf "%${indent}s" ""は、指定した数値分の半角スペースを作るテクニックです。%10sで指定すると「10文字分の幅確保」という意味になります。そこに空文字""を流し込むことで、結果的に指定した数(indent)だけの空白が出力されます。 -
if ((i == 0))(最初の行)だけは、trで空白をすべて█に置き換え、塗りつぶします。2行目以降は、左右の両端だけを█にし、間を空白にすることで、中が空っぽの器を表現しています。
ビールが横から流れてくる演出は以下のようにして行います。
function draw_mug() {
local x_left=$1 #左端の位置
local y0=$2 #底の位置
buffer="${BLUE}\033[H"
for ((i = 0; i < H_MUG; i++)); do
full_line="${MUG_LINES[i]}"
y=$((y0 - i))
if ((x_left < 1)); then
offset=$((1 - x_left))
draw_line="${full_line:$offset}"
draw_x=1
else
draw_line="$full_line"
draw_x=$x_left
fi
buffer+="\033[${y};1H\033[K\033[${y};${draw_x}H${draw_line}"
done
echo -ne "$buffer${RESET}"
}
-
x_leftが左端、y0が下を表しており、引数で受け取ります。 -
buffer+="\033[${y};1H\033[K\033[${y};${draw_x}H${draw_line}"は一見複雑ですが、-
\033[${y}でy行、1列まで移動 -
\033[Kで行末までクリア -
\033[${y};${draw_x}Hでy行draw_x列まで移動 -
draw_lineを表示
という意味です。
-
- すべて連結した
bufferを最終的にecho -ne "$buffer${RESET}"で描画しています。
x_leftを徐々に動かすことで、

このようにジョッキを右に移動させることができます。
注ぎ口の描画
注ぎ口もいままで使った技を駆使していきます。
function draw_spout() {
offset_x=$1
offset_y=$2
flg=$3
tput cup $offset_y $offset_x
if ((flg == 1)); then
FIG_SPOUT="/ /${UP}${LEFT}${LEFT}${LEFT}${LEFT}"
FIG_SPOUT+="/ /${UP}${LEFT}${LEFT}${LEFT}${LEFT}"
FIG_SPOUT+="/ /${UP}${LEFT}${LEFT}${LEFT}${LEFT}"
FIG_SPOUT+="| |"
echo -ne "${FIG_SPOUT}"
else
#クリア
FIG_SPOUT=" ${UP}${LEFT}${LEFT}${LEFT}${LEFT}"
FIG_SPOUT+=" ${UP}${LEFT}${LEFT}${LEFT}${LEFT}"
FIG_SPOUT+=" ${UP}${LEFT}${LEFT}${LEFT}${LEFT}"
FIG_SPOUT+="Cheers!!!!"
echo -ne "${FIG_SPOUT}"
fi
}
-
offset_xとoffset_yの引数を受け取り、tput cup $offset_y $offset_xでカーソル移動 -
flgは描画するかクリアするかのフラグ
FIG_SPOUT="/ /${UP}${LEFT}${LEFT}${LEFT}${LEFT}"
FIG_SPOUT+="/ /${UP}${LEFT}${LEFT}${LEFT}${LEFT}"
FIG_SPOUT+="/ /${UP}${LEFT}${LEFT}${LEFT}${LEFT}"
FIG_SPOUT+="| |"
echo -ne "${FIG_SPOUT}"
ここはカーソル移動を駆使して、注ぎ口を描画しています。
最終的にはこんな感じになります。
ビールが注がれる演出と溜まっていくビールの描画
コードは以下のとおりです。※長いので折りたたんでいます。
描画のコード
bottom_beer=$((offset_y)) #ビールの底
total_time=5 #全体の時間
bottom_frame=30 #ビールが底に達するまでのフレーム
num_frame=60 #1秒間のフレーム
draw_spout $offset_x $offset_y 1 #最後の引数は描画するか、クリアするかのフラグ
draw_mug $((MOVE_SPEED - 1)) $y0 # ビールの描画
for ((f = 0; f < $((total_time * num_frame)); f++)); do
bottom_beer=$((offset_y - (H_MUG - 3) * f / bottom_frame)) #注がれるビールの液面
if ((bottom_beer <= $((offset_y - H_MUG - 3)))); then
# 液体の描画
current_height=$((H_MUG * 150 * (f - bottom_frame) / num_frame / total_time / 100))
if ((current_height > H_MUG)); then
current_height=H_MUG
# 注ぎ口を消す
tput cup $((offset_y + 1)) $offset_mug_x
line="${YELLOW}"
for ((o = 0; o < MOVE_SPEED; o++)); do
line+="${RIGHT}"
done
for ((h = 0; h < 4; h++)); do
for ((w = 0; w < 4; w++)); do
line+=" "
done
line+="${DOWN}"
for ((w = 0; w < 4; w++)); do
line+="${LEFT}"
done
done
line+="${RESET}"
echo -ne "$line"
fi
beer=""
tput cup $((Y_BOTTOM - 2)) $((MOVE_SPEED - 1))
for ((h = 1; h < current_height; h++)); do
offset_beer_x=$(((WS_MUG[H_MUG - 1] - WS_MUG[h]) / 2))
if ((h <= H_MUG - 5)); then
#上から3つは泡にしたい
for ((w = 0; w < offset_beer_x; w++)); do
beer+="${RIGHT}"
done
f_h=$((h * num_frame * total_time * 100 / 150 / H_MUG + bottom_frame)) #その高さがくる時間
p_y=$((3000 * (f - f_h) / num_frame / total_time)) # 確率小数扱えないので、100倍
for ((w = 0; w < WS_MUG[h] - 2; w++)); do
if ((RANDOM % 1000 <= p_y)); then
beer+="${YELLOW}█${RESET}"
else
beer+="${WHITE}█${RESET}"
fi
done
beer+="${UP}"
for ((w = 0; w < WS_MUG[h] - 2; w++)); do
beer+="${LEFT}"
done
for ((w = 0; w < offset_beer_x; w++)); do
beer+="${LEFT}"
done
else
for ((w = 0; w < offset_beer_x; w++)); do
beer+="${RIGHT}"
done
f_h=$((h * num_frame * total_time * 100 / 150 / H_MUG + bottom_frame)) #その高さがくる時間
p_y=$((10 * (f - f_h) / num_frame / total_time)) # 確率小数扱えないので、100倍
for ((w = 0; w < WS_MUG[h] - 2; w++)); do
if ((RANDOM % 1000 <= p_y)); then
beer+="${YELLOW}█${RESET}"
else
beer+="${WHITE}█${RESET}"
fi
done
beer+="${UP}"
for ((w = 0; w < WS_MUG[h] - 2; w++)); do
beer+="${LEFT}"
done
for ((w = 0; w < offset_beer_x; w++)); do
beer+="${LEFT}"
done
fi
done
echo -ne "$beer"
else
# 注ぎ口の描画
tput cup $((offset_y + 1)) $offset_mug_x
line="${YELLOW}"
for ((o = 0; o < MOVE_SPEED; o++)); do
line+="${RIGHT}"
done
for ((h = offset_y; h >= bottom_beer; h--)); do
for ((w = 0; w < 4; w++)); do
line+="█"
done
line+="${DOWN}"
for ((w = 0; w < 4; w++)); do
line+="${LEFT}"
done
done
line+="${RESET}"
echo -ne "$line"
fi
sleep 0.008
done
draw_spout $((offset_x)) $offset_y 0
ビールが注がれる演出
# 注ぎ口の描画
tput cup $((offset_y + 1)) $offset_mug_x
line="${YELLOW}"
for ((o = 0; o < MOVE_SPEED; o++)); do
line+="${RIGHT}"
done
for ((h = offset_y; h >= bottom_beer; h--)); do
for ((w = 0; w < 4; w++)); do
line+="█"
done
line+="${DOWN}"
for ((w = 0; w < 4; w++)); do
line+="${LEFT}"
done
done
line+="${RESET}"
echo -ne "$line"
-
offset_yがビール液面の高さで、ビール底より高かったら注ぎ続けます。 - 注ぐビールは
█で、カーソル移動を駆使して全体を表現しています。
溜まってくビールの描画
溜まっていくビールは、基本的にジョッキの横幅に合わせて増やしていくのですが、ビールの泡の多さにこだわりがあります。
f_h=$((h * num_frame * total_time * 100 / 150 / H_MUG + bottom_frame)) #その高さがくる時間
p_y=$((3000 * (f - f_h) / num_frame / total_time)) # 確率小数扱えないので、100倍
for ((w = 0; w < WS_MUG[h] - 2; w++)); do
if ((RANDOM % 1000 <= p_y)); then
beer+="${YELLOW}█${RESET}"
else
beer+="${WHITE}█${RESET}"
fi
done
- この部分で、ビール液面高さ
hの時間f_hを計算し、現在時刻fがf_hより十分大きかったらp_yを大きくします。 -
p_yはYELLOWすなわち黄色の確率を示しており、この値が大きいほど、高さhでの黄色の数が多いということになります。 -
RANDOM % 1000 <= p_yこちらもShell Scriptでの確率の表現のコツです。
全体のコード
全体のコードは
全体のコード
# !/bin/bash
readonly H_TER=$(tput lines) #ターミナルの縦幅
readonly W_TER=$(tput cols) #ターミナルの横幅
w1=$((W_TER * 30 / 100)) #計算用w
readonly MIN_W_MUG=$((w1 % 2 == 0 ? w1 : w1 + 1)) #ビールジョッキの最大の横幅(形は後で変える)必ず偶数
readonly H_MUG=$((H_TER * 40 / 100)) #ビールジョッキの縦幅
readonly Y_BOTTOM=$((H_TER - 5)) #ジョッキの底の位置
readonly POUR_SPEED=500 #注ぐ速度
readonly MOVE_SPEED=$((MIN_W_MUG / 3 * 2)) #ビールジョッキ登場スピード
readonly YELLOW="\033[33m" #黄色
readonly BLUE="\033[34m" #黄色
readonly WHITE="\033[37m" #白色
readonly RESET="\033[0m" #リセット
readonly UP="\033[1A" #カーソル上
readonly DOWN="\033[1B" #カーソル下
readonly RIGHT="\033[1C" #カーソル右
readonly LEFT="\033[1D" #カーソル左
#ビールジョッキの横幅
function get_beer_w() {
local h=$1 #ある高さ
local w=$((h * 60 / 100 + MIN_W_MUG)) #その時の横幅
if ((w % 2 == 1)); then
#奇数だとズレちゃう
w=$((w + 1))
fi
echo "$w"
}
WS_MUG=()
for ((i = 0; i < H_MUG; i++)); do
WS_MUG[i]=$(get_beer_w "$i")
done
MUG_LINES=()
W_BASE=$((H_MUG * 60 / 100 + MIN_W_MUG)) # 1番広い幅
if ((W_BASE % 2 == 1)); then
W_BASE=$((W_BASE + 1))
fi
for ((i = 0; i < H_MUG; i++)); do
w_current=${WS_MUG[i]}
indent=$(((W_BASE - w_current) / 2))
line=$(printf "%${indent}s" "") #左インデント
if ((i == 0)); then
line+=$(printf "%${w_current}s" "" | tr ' ' '█')
else
inner_w=$((w_current - 2))
line+="█"$(printf "%${inner_w}s" "")"█"
fi
MUG_LINES[$i]="$line"
done
#ジョッキの描画
function draw_mug() {
local x_left=$1 #左端の位置
local y0=$2 #底の位置
buffer="${BLUE}\033[H"
for ((i = 0; i < H_MUG; i++)); do
full_line="${MUG_LINES[i]}"
y=$((y0 - i))
if ((x_left < 1)); then
offset=$((1 - x_left))
draw_line="${full_line:$offset}"
draw_x=1
else
draw_line="$full_line"
draw_x=$x_left
fi
# \033[Kは行末までクリア、\033[y;xHは座標指定
buffer+="\033[${y};1H\033[K\033[${y};${draw_x}H${draw_line}"
done
echo -ne "$buffer${RESET}"
}
#注ぎ口の描画
function draw_spout() {
offset_x=$1
offset_y=$2
flg=$3
tput cup $offset_y $offset_x
if ((flg == 1)); then
FIG_SPOUT="/ /${UP}${LEFT}${LEFT}${LEFT}${LEFT}"
FIG_SPOUT+="/ /${UP}${LEFT}${LEFT}${LEFT}${LEFT}"
FIG_SPOUT+="/ /${UP}${LEFT}${LEFT}${LEFT}${LEFT}"
FIG_SPOUT+="| |"
echo -ne "${FIG_SPOUT}"
else
#クリア
FIG_SPOUT=" ${UP}${LEFT}${LEFT}${LEFT}${LEFT}"
FIG_SPOUT+=" ${UP}${LEFT}${LEFT}${LEFT}${LEFT}"
FIG_SPOUT+=" ${UP}${LEFT}${LEFT}${LEFT}${LEFT}"
FIG_SPOUT+="Cheers!!!!"
echo -ne "${FIG_SPOUT}"
fi
}
##########################################
# 初期
##########################################
tput clear #画面をクリア
tput civis #カーソル消す
##########################################
# ビールジョッキを左から登場させる
##########################################
x0=0 # 基準
y0=$Y_BOTTOM # 基準
offset_mug_x=$(((WS_MUG[H_MUG - 1] - WS_MUG[0]) / 2 + WS_MUG[0] / 2)) #ジョッキの左端
offset_y=$((Y_BOTTOM - H_MUG - 5)) #注ぎ口の下端
offset_x=$((offset_mug_x + MOVE_SPEED)) #注ぎ口の左端
for ((x = $((-W_BASE - 10)); x < MOVE_SPEED; x++)); do
draw_mug $x $y0 # ビールの描画
draw_spout $((offset_x)) $offset_y 1
# 注ぎ口の描画
sleep 0.008
done
# 注ぎ描画 30フレームで5秒間
bottom_beer=$((offset_y))
total_time=5
bottom_frame=30
num_frame=60
draw_spout $offset_x $offset_y 1 #最後の引数は描画するか、クリアするかのフラグ
draw_mug $((MOVE_SPEED - 1)) $y0 # ビールの描画
for ((f = 0; f < $((total_time * num_frame)); f++)); do
bottom_beer=$((offset_y - (H_MUG - 3) * f / bottom_frame)) #注がれるビールの液面
if ((bottom_beer <= $((offset_y - H_MUG - 3)))); then
# 液体の描画
current_height=$((H_MUG * 150 * (f - bottom_frame) / num_frame / total_time / 100))
if ((current_height > H_MUG)); then
current_height=H_MUG
#注ぎを消す
tput cup $((offset_y + 1)) $offset_mug_x
line="${YELLOW}"
for ((o = 0; o < MOVE_SPEED; o++)); do
line+="${RIGHT}"
done
for ((h = 0; h < 4; h++)); do
for ((w = 0; w < 4; w++)); do
line+=" "
done
line+="${DOWN}"
for ((w = 0; w < 4; w++)); do
line+="${LEFT}"
done
done
line+="${RESET}"
echo -ne "$line"
fi
beer=""
tput cup $((Y_BOTTOM - 2)) $((MOVE_SPEED - 1))
for ((h = 1; h < current_height; h++)); do
offset_beer_x=$(((WS_MUG[H_MUG - 1] - WS_MUG[h]) / 2))
if ((h <= H_MUG - 5)); then
#上から3つは泡にしたい
for ((w = 0; w < offset_beer_x; w++)); do
beer+="${RIGHT}"
done
f_h=$((h * num_frame * total_time * 100 / 150 / H_MUG + bottom_frame)) #その高さがくる時間
p_y=$((3000 * (f - f_h) / num_frame / total_time)) # 確率小数扱えないので、100倍
for ((w = 0; w < WS_MUG[h] - 2; w++)); do
if ((RANDOM % 1000 <= p_y)); then
beer+="${YELLOW}█${RESET}"
else
beer+="${WHITE}█${RESET}"
fi
done
beer+="${UP}"
for ((w = 0; w < WS_MUG[h] - 2; w++)); do
beer+="${LEFT}"
done
for ((w = 0; w < offset_beer_x; w++)); do
beer+="${LEFT}"
done
else
for ((w = 0; w < offset_beer_x; w++)); do
beer+="${RIGHT}"
done
f_h=$((h * num_frame * total_time * 100 / 150 / H_MUG + bottom_frame)) #その高さがくる時間
p_y=$((10 * (f - f_h) / num_frame / total_time)) # 確率小数扱えないので、100倍
for ((w = 0; w < WS_MUG[h] - 2; w++)); do
if ((RANDOM % 1000 <= p_y)); then
beer+="${YELLOW}█${RESET}"
else
beer+="${WHITE}█${RESET}"
fi
done
beer+="${UP}"
for ((w = 0; w < WS_MUG[h] - 2; w++)); do
beer+="${LEFT}"
done
for ((w = 0; w < offset_beer_x; w++)); do
beer+="${LEFT}"
done
fi
done
echo -ne "$beer"
else
# 注ぎ口の描画
tput cup $((offset_y + 1)) $offset_mug_x
line="${YELLOW}"
for ((o = 0; o < MOVE_SPEED; o++)); do
line+="${RIGHT}"
done
for ((h = offset_y; h >= bottom_beer; h--)); do
for ((w = 0; w < 4; w++)); do
line+="█"
done
line+="${DOWN}"
for ((w = 0; w < 4; w++)); do
line+="${LEFT}"
done
done
line+="${RESET}"
echo -ne "$line"
fi
sleep 0.008
done
draw_spout $((offset_x)) $offset_y 0
tput cup $H_TER 0
tput cnorm #カーソル表示
これを.sh形式で保存して、
bash 〇〇.shで実行すればビールが登場します。
おわりに
最後までお付き合いいただきありがとうございました!
まだまだ完成度が低いですので、もっとこうすればいいでは?こういうものを作ってほしいというアイデアや実装例、募集中です!
- ランダムで泡の量を変える
- ジョッキを好きな形にする
- 黒ビールに対応する
等々やりたいことはまだまだ盛り沢山です。

