【シェルと遊ぼう】sleepコマンドでタイマースクリプトを作ってみよう(on tmux)

| ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄|  
| TIME OUT |  
|__________|  
(\__/) ||  
(•ㅅ•) ||  
/   づ  

はじめに

「好奇心」というのは大切だと思う。

しかし同時に、厄介なものでもある。

例えば、コードを書こうとエディタを開く。

書いている途中、エディタの挙動がおかしいことに気付く。
よく分からないのでネットで調べてみる。
見ていたサイトで、関係のない面白そうな設定を見つける。

ワクワクしながら試してみる。

うーん、動かない。。。何が原因?
と、その設定について調べはじめる。

調べて試してを何度か繰り返し、ようやく動く。

めでたしめでたし。

さて、、、あれ、そもそも何してたんだっけ?

あーそうだそうだ、コードを書いていたんだった。
と、コードを書こうとエディタを開く。

書いている途中、エディタの挙動がおかしいことに (以下略


この厄介な「好奇心」と付き合っていくために、「作業を時間で区切る」という習慣を付けたいと思う。

先の例で言えば、今から調べものを10分だけやるぞ。
と、決めておけば、10分後に進捗と作業を見直す機会を持てる。

例え途中で横道にそれたとしても、10分後に当初の目的を思い出すことができそうだ。

よし、じゃあまずは、時間を計って経過時間後に通知するシェルスクリプトを作ろう。

sleepコマンドを使えば、割と簡単に作れそうだ。

と、おもちゃを作る動機づけはこんなとこ。 やってみよう。

つくるもの

実行例

今回作るものは、こんな感じで使える。 ちょっとしたスクリプトを用意すれば繰り返し実行もできる。
(以降、tellという名前でシェルスクリプトを作成した前提で説明)

> tell 10m                       # 10分後にデフォルトメッセージを表示  
> tell 10m "THE END"             # 10分後に指定したメッセージを表示  
> tell 1h -c "tmux lock-server"  # 1時間後に任意のコマンドを実行  

指定時間経過後に、下記のようなうさぎちゃんが問答無用で全力で割り込んでくる。

なかなか鬱陶しくも、愛おしい。

| ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄|  
|  take a  |  
|  break!  |  
|__________|  
(\__/) ||  
(•ㅅ•) ||  
/   づ  

メッセージは、新たに作成するtmuxのウィンドウに出力している。

うさぎちゃんは、bunnysayコマンドによって出力している。

また、任意のコマンド指定時は、上記メッセージは表示せずに指定したコマンドを実行するだけ。

ヘルプメッセージ

tell wait_time[hms] [-c command | message]  
    wait_time   wait time[unit]  
        [unit]  
           h:   hours = wait_seconds * 3600 [sec]  
           m:   minutes = wait_seconds * 60 [sec]  
           s:   seconds = wait_seconds [sec] (default)  

    -c command  execute command after wait_time  
                default) tmux new window & popup message  

    message     any message  

必要なも

今回のスクリプトでは、以下のソフトを利用する。

  • tmux   CUIで画面分割やタブ作成みたいなことができる子
  • bunnysay うさぎちゃんがメッセージを表示してくれる

FreeBSDなら、下記コマンドでインストールできる。(root権限で実行してね)

> pkg tmux bunnysay  

上記ソフトがない場合、指定時間経過後にechoでメッセージを出力するだけの寂しい動作になる。
それに、エディタなどを開いていると、悲惨な表示になる。

とはいえ、そこはシェルスクリプト
あなたの好きなコマンドに置き換えることは簡単だろう。

コード

今回用意したコードはこちら。

長いけど、結局やっていることは最後のsleepコマンドの1行。
あとは、そこに渡すパラメータ作成が大半。

何か変なところがあれば、ご指摘頂けると幸いです。

ざっくり処理ブロックを説明すると、以下となる。

  • 2行目以降 ヘルプ作成
  • 20行目以降 引数をsleepコマンドに渡す秒単位に変換
  • 35行目以降 不正な時間指定だった場合、エラーメッセージを出して終了
  • 43行目以降 指定時間経過後に、実行するコマンドを設定
  • 50行目以降 tmuxで新規ウィンドウを作成し、メッセージを出力する
  • 70行目以降 バックグラウンドでsleepコマンドにより待機後、コマンドを実行

少し込み入っている以下について補足しておこう。

  • 引数の時間変換
  • tmuxのウィンドウ作成

補足) 時間の変換

引数の時間を秒単位に変換する処理は、20行目以降の下記コード。

# set wait seconds
if [ ${#1} -ge 2 ]; then  
    target_time=`echo "$1" | cut -c 1-\`expr ${#1} - 1\``  
fi  
case "$1" in  
    *h ) wait_sec=`expr $target_time \* 3600`  
        ;;  
    *m ) wait_sec=`expr $target_time \* 60`  
        ;;  
    *s ) wait_sec="$target_time"  
        ;;  
    *  ) wait_sec="$1"  
        ;;  
esac  

# wait_sec is digit?
if [ -z "`echo "$wait_sec" | grep '^[0-9]\{1,\}$'`" ]; then  
    echo "Invalid wait time!: `$1`"  
    usage_exit  
fi  

最初のifブロックでは、cutコマンドで引数の指定時間から末尾1文字を省いた数値部分を抽出している。
1文字の場合、cutでエラーが出ないようにifで囲っている。
(cut -c 1-0という不正な指定になってしまう)

もし、2文字以上の数値だけの場合は、この変数は実際には使わない。
次のcase文の*のパターンで、引数を待機秒数にセットする。

case文の*h,*mのパターンでは、それぞれ360060をかけて秒単位に変換している。
*sのパターンでは、末尾からsを除いた数値部分をそのまま待機秒数にセットしている。

最後のifで、「1文字以上の数字の繰り返し」以外だった場合は、エラーメッセージで終了する。

補足) tmuxのウィンドウ作成

tmuxのウィンドウ作成は、下記関数で行っている。

popup_window() {
    [ -n "$*" ] && msg="$*" || msg="take a break!"
    [ -n "`which bunnysay`" ] && print_cmd=bunnysay || print_cmd=echo

    # make popup window
    tmux new-window 2> /dev/null && (
        WIN_ID=`tmux display -p '#I'`

        # print message
        tmux pipe-pane -I -t:$WIN_ID 'echo " "'
        tmux pipe-pane -I -t:$WIN_ID "echo '$print_cmd \"$msg\"'"
        # tmux clock-mode -t:$WIN_ID 2> /dev/null

        # auto close
        sleep 3
        tmux kill-window -t:$WIN_ID
    ) 2> /dev/null \
    || ( echo " "; $print_cmd "$msg" ) 1>&2
}

最初の処理の2行は、メッセージとメッセージを出力するコマンドの設定。

引数があれば引数のメッセージを、なければ既定のメッセージをセットする。
bunnysayがあればbunnysayを、なければechoを出力コマンドにセットしている。

tmux new-windowの行で、tmuxのウィンドウを新たに作成している。
作成に成功した場合は、&& (〜)ブロックの中の処理を実行する。
失敗した場合は、最後の行の|| (〜)ブロックの中の処理を実行する。
つまり、tmuxが動いてないときはechoするだけになる。

&& (〜)ブロックの中では、まず作成したウィンドウのIDを取得しておく。
以降のtmuxのコマンドでは、-tオプションにで作成したウィンドウに対してコマンドを実行する。

次に、tmux pip-paneコマンドで、作成したウィンドウにメッセージを出力している。
最初のecho " "は、最初に改行を出力するためのもの。なかなかの手抜き感。

コメントアウトしているが、tmux clock-modeにより全画面で時間を表示をすることもできる。
ただし、何かキーを押したらすぐに消えてしまうため、実用性はあまりないかもしれない。

最後に、sleepコマンドで待機後、作成したウィンドウを自動で閉じる。
待機時間は好きに変えていいし、不要なら削除するといい。
オプションで選択できるようにしてもいいかもしれない。

なお、ブロック中の標準エラー出力/dev/nullに捨てている。

連続実行による途中経過時間の表示と、繰り返しと、中断と

下記のような別のシェルスクリプトを用意しておけば、途中経過時間も表示できたりする。
さらに、ひたすら繰り返せる。
経過表示分のプロセスを追加で立ち上げることになるけれども。

#!/bin/sh

tell 10m "10 min"  
tell 20m "20 min"  
tell 25m  
tell 30m "Get to work!"  
tell 30m -c "$0"  

でも止める場合killが大変なので、以下のようなスクリプトも用意した方が良いかも。
(fzf利用しているので、インストールして試してね=>こちらの記事など参考)

#!/bin/sh
selected=`ps ax \  
    | grep tell \  
    | grep -v "grep tell" \  
    | grep -v "$0" \  
    | fzf -m`  
[ -z "$selected" ] && exit 0 

echo "$selected" \  
    | sed 's/^[ ]*\([0-9]\{1,\}\)[^ ]*.*$/\1/' \  
    | xargs kill  

psコマンドの出力がFreeBSD以外でどうなるか自信がないので、Linuxなどの人は必要ならsedのパターンを置換えてほしい。

それにしても、一括でkillする方法ないのかな。
fzfを使っているので、だいぶ楽ではあるのだけれども。

何か素敵殺法をご存知の方は、コメント頂けると幸いです。

実際に使ってみると

今回のスクリプトは、デフォルトではどんな作業をしていようと、急にウィンドウを切り替えて、全力でうさきちゃんが邪魔をしてくる。

作業タイマーとしては邪魔かもしれない。
実用としては音を鳴らすなど、お好みの通知方法に変えてほしい。

また、通知の画面切り替えの際にtmuxのウィンドウ操作をごちゃごちゃやると、きっとバグる。
ウィンドウIDを指定して多少はバグりにくくしているものの、完全ではないと思うので悪しからず。

最後に

もし、今回のスクリプトに「何時」という時間指定で予約する機能を追加したい場合、どうすればいいだろうか。
シェルスクリプトで時間の計算は面倒臭そうだ。
何かコマンドに頼るのものいいかもしれないが、datesleepしながらポーリングする方法なら楽に実装できる気がする。
興味がある人は、チャレンジしてみてはいかがだろうか。

 
 

興味。

 
 

それは「好奇心」がある限り尽きることはない。

ただ、残念ながら人生で使える時間は有限だ。

一生かかっても知らないことや理解できないことの方が多い。

時間というものさしを使いながら、目の前のお仕事に集中しましょ。

確認環境

PC Thinkpad X1 Carbon 2nd Gen
OS 12.0-RELEASE-p7
tmux tmux 2.9a
bunnysay 1.1_2