【Mac】ターミナルでカウントダウン

前に、カップメンの 3 分間をターミナルで測れたらいいのに、という話で、

sleep 180; say 'ラーメンが出来ました'

というのを紹介した(【Mac】長ったらしいコマンドを短く | 林檎コンピュータ)。
 この 180 秒というのを、カウントダウンさせて表示したい、と思う。

シェルスクリプト

🍜 ramen.sh
#!/bin/bash

MIN=$1
jikan=`echo $((MIN * 60))`

date

while [ 0 -lt $jikan ]
do
	echo -en ' 🍜 '"$jikan\r"
	sleep 1
	jikan=`echo $((jikan - 1))`
done

date
say '時間です'

肝となるのは、

echo -en ' 🍜 '"$jikan\r"

の、 -e オプションと \r である。

エスケープ文字を出力する

-e オプションは、バックスラッシュでエスケープされた、エスケープ文字というものを出力する。
  \r はカーソルを行頭に戻してくれるエスケープ文字だ。キャリッジリターンというらしい。 -e をつけることで、キャリッジリターンを機能させることが出来る。
 たとえば \n は UTF-8 において改行を意味するエスケープ文字として有名だ。これを echo するには -e オプションをつける。

# \n が文字列として出力される。
MacBook:~ user$ echo "hello\nworld"⏎
hello\nworld

# \n が改行として出力される。
MacBook:~ user$ echo -e "hello\nworld"⏎
hello
world

ちなみに echo -n は出力を改行させないオプションだ。
 キャリッジリターンは、カーソルを行頭に戻して次の出力を待つ。
 以下を試すと 1s……2s……3s と一秒ごとに表示が移りかわっていく。

echo -en " 1s\r"; sleep 1; echo -en  " 2s\r"; sleep 1; echo ' 3s'

上掲した ramen.sh で注意すべきは、 #!/bin/sh だと動かない(おもに Mac においてだと思われる)。

-en 59
-en 58
-en 57
……

などと表示されてしまう。 #!/bin/bash だとちゃんと動く。
 もしくは、 printf を使うといいらしい。

$ printf "彼女が\n好き\n"⏎
彼女が
好き

$ printf "彼女が好き\r彼氏\n"⏎
彼氏が好き

エスケープ文字は、ほかにもいくつかある。

# \a
# アラームを鳴らす。
$ echo -e "\a"

# \b
# バックスペース
MacBook:~ user$ echo -e "abcdefg\b\b"
abcdeMacBook:~ user$

# \c
# 改行しない
MacBook:~ user$ echo -e "改行なし\c"
改行なしMacBook:~ user$ 

# \f 
# 改ページ(フォームフィード)
MacBook:~ user$ echo -e "\f"


MacBook:~ user$

# \n
# 改行
$ echo -e "ここで\nはきものをぬいでください"
ここで
はきものをぬいでください

# \r
# キャリッジリターン
$ echo -e "明日やろう\r馬鹿"
馬鹿やろう

# \t
# タブ 
MacBook:~ amenecobook$ echo -e "\t--遠い昔、遥か彼方の銀河系で"
	--遠い昔、遥か彼方の銀河系で
	
# \v
# 縦のタブ
MacBook:~ user$ echo -e "\v"


MacBook:~ user$

# \0nnn
# 8 進数の ASCII 文字
$ echo -e "\0102\0101\0113\0101"
BAKA

# \xnn
# 16 進数の ASCII 文字
$ echo -e "\x41\x48\x4f"
AHO

# \\
# バックスラッシュ
$ echo -e "\\"
\

シェルスクリプトの修正

上記スクリプトファイルは、平気で数秒ずれる。ひどい出来だ。
 また、ひと桁秒の表示がなんかおかしい。
 以下のように書き直した。

🍜 ramen.sh
#!/bin/bash

MIN=$1
jikan=`echo $((MIN * 60))`
Now=`date "+%s"`
target=`echo $((Now + jikan))`

date

while [ 0 -lt $jikan ]
do
	fun=`echo $((jikan / 60))`
	byo=`echo $((jikan % 60))`
	echo -en ' 🍜 '"$fun 分 $byo 秒\r"
	sleep 1
	Now=`date "+%s"`
	jikan=`echo $((target - Now))`
done

date
osascript -e 'display notification "時間です。"'
say '時間です'

これで多少マシになった、と思う。

時刻を入力してカウントダウン

ramen.sh は分を引数にしてカウントダウンしたが、そうではなく、時間を入力してアラームを設定したい場合もある。
 夜、七時半に家を出なくてはならないとき、 alerm.sh 1930 とやって時刻を知らせてくれれば便利だろう。
 以前、カウントダウンに関するスクリプトを書いたので、それを流用して作ってみる。

⏱ alerm.sh
#!/bin/bash

function keisan() {
	time=$1

	fun=`echo $((time/60))`
	byo=`echo $((time%60))`

	if test $fun -ge 60; then
		ji=`echo $((fun/60))`
		fun2=`echo $((fun%60))`
		if test $ji -ge 24; then
			nichi=`echo $((ji/24))`
			ji2=`echo $((ji%24))`
			echo "${nichi}"'日'"${ji2}"'時間'
		else
			echo "${ji}"'時間'"${fun2}"'分'
		fi
	else
		echo "${fun}"'分'"${byo}"'秒'
	fi	
}

if test -z "$1"; then
	echo '時刻を、24時間表記の 4 桁の数字で入力してください'
	exit 1
else
	jikoku=$1
fi

if [[ "$jikoku" =~ ^[0-9]+$ ]]; then #引数が数字かどうか
	keta=`echo ${#jikoku}` #桁数を調べる
	if ! test "$keta" -eq 4; then
		echo '時刻を、24時間表記の 4 桁の数字で入力してください'
		exit 1
	fi
else
	echo '時刻を、24時間表記の 4 桁の数字で入力してください'
	exit 1
fi

nowYMD=`date "+%Y%m%d"`
tgTime=`echo "$nowYMD""$jikoku"'00'`
target=`date -j -f "%Y%m%d%H%M%S" "$tgTime" "+%s"`

nowTime=`date "+%s"`
nokori=`echo $((target - nowTime))`

# 残り時間がマイナスなら、明日とみて 86400 秒足す。
if test $nokori -le 0; then
	target=`echo $((target + 86400))`
	nokori=`echo $((target - nowTime))`
fi

targetJST=`date -r "$target"`
echo "$targetJST" 'まで'

while [ 0 -lt $nokori ]
do
	seconds=`keisan "$nokori"`
	echo -en ' > '"$seconds\r"
	sleep 1
	nowTime=`date "+%s"`
	nokori=`echo $((target - nowTime))`
done
date
say '時間です'

対応しているのは 24 時間以内の時間だ。
 引数の入力は 0000 〜 2359 までの 24 時間表記。コロンなどは入れない。

say '時間です'

ではなく、

afplay 音声ファイル

のほうがいいかもしれない。けっこう聞き逃すので。
 途中でキャンセルしたくなったら、control + c を押す。