しかくいさんかく

解答略のメモ

シェルに時刻を表示する最強の方法

この記事は dotfiles Advent Calendar 2019 - Qiita の25日目の記事です。 zshに関する話題です。

今日は某有名人の誕生日ということで、時刻に関する話題を扱いたいのですが、 UNIXのシェルを操作する場面では

  • 今何時?
  • あのコマンド叩いたのいつだっけ?
  • コマンドの実行に何分かかった?

これらの時間を意識することが多いでしょう。 そんなとき、今何時か知りたければdateと叩き、コマンドの開始時刻を知りたければhistoryで検索し1、実行時間の計測にはtimeを使うというのが、古式ゆかしいUNIXのシキタリといえます。 たくさんあってややこしいですね!

例として、sleep 5というコマンドの実行時間を測ってみましょう。 もちろん5秒ジャストになるはずなのですが、これを計るにはコマンドの前方にtimeと書く必要があります。

$ time sleep 5
sleep 5  0.00s user 0.00s system 0% cpu 5.008 total

こんな感じで実行時間を計測できます。 どうやらsleep 5の実行には5.008秒を要したようです。 5秒ジャストじゃないんだ。へ~~

ところがこのtimeは癖者で、ついつい 「うわ!!コマンドの実行時間を測りたかったのに、timeをつけ忘れた!!でももう実行してから何時間も経ってるし、ここで止めてやり直すのはヤダナ~」 みたいになりがちです。

$ sleep 10000d
$

↑実行に10000日を要するコマンドが、無事完了して感無量なのですが、しかしtimeをつけ忘れるという痛恨のミスをやらかした図。このタイムロスは痛すぎる。。。

改善方法

timeのつけ忘れを防ぐのは簡単です。 そもそもtimeコマンドを使わなければよいのです。 代わりに シェルのプロンプトに現在時刻を表示 すれば問題が解決します。 実行前のプロンプトの時刻と、実行後のプロンプトの時刻を比較(引き算)すれば、コマンドの実行時間が計算できます。 こうすることで、冒頭の「今何時?あのコマンド叩いたのいつだっけ?」という疑問まで解決するし、一挙三得で超うれし~~

というような記事が量産されているのですが、私の読んだ限り、イマイチな設定例が多いように思われます。 普通にコマンドの終了時刻を表示している例が多いわけですが、、、、、

考えろ!考えるんだっっ!!よくよく考えると、我々はUNIXシェルの前で

  1. 前回のコマンドが終わる (プロンプトに入る)
  2. キーボードを叩いて、次回のコマンドを入力
  3. エンターキーを、ッターーン!!!
  4. 次回のコマンドが始まる (プロンプトから出る)

つまりプロンプトに入る時刻(1)と、出る時刻(4)は別物です。 これら両方を知っておかないと、コマンドの実行時間は計測できません。 そう、つまり、我々は、入室と退室の2つの時刻を表示する必要に迫られているのです。 それこそが論理的に正しく、真にあるべき姿だと言えましょう。

要するに、欲しいのはコレだ!!

f:id:kaitou_ryaku:20191222151745g:plain

  • 左側の数字が、プロンプトに入った時刻(前回のコマンドが終了した時刻)です。
  • 右側の数字が、プロンプトから出た時刻(次回のコマンドを開始する時刻)です。

図の読み方ですが、簡単な引き算で諸々に費やした時間を計算できます。

f:id:kaitou_ryaku:20191222153110p:plain

コマンドに色がついてるのは zsh-syntax-hilighting のおかげですが、今回の記事にはあまり関係ないですね。

さて設定方法ですが、以下の設定を$HOME/.zshrcに記述すれば実現できます。

export PREV_COMMAND_END_TIME
export NEXT_COMMAND_BGN_TIME

function show_command_end_time() {
  PREV_COMMAND_END_TIME=`date "+%H:%M:%S"`
  PROMPT="${PREV_COMMAND_END_TIME} -          $ "
}
autoload -Uz add-zsh-hook
add-zsh-hook precmd show_command_end_time

show_command_begin_time() {
  NEXT_COMMAND_BGN_TIME=`date "+%H:%M:%S"`
  PROMPT="${PREV_COMMAND_END_TIME} - ${NEXT_COMMAND_BGN_TIME} $ "
  zle .accept-line
  zle .reset-prompt
}
zle -N accept-line show_command_begin_time

もっとクールにしたい

ここまで見てきた設定で、時刻が表示されて嬉しいわけですが、なんかダサい。 そこで私は以下のような設定で生活しています。

# 色の設定。^[はエスケープ文字
COLOR_BGN="%{^[[044m%}" # 青色。数字を変えて、サーバー毎に色を変えよう
COLOR_END="%{^[[m%}"
COLOR_DEFAULT="%{^[[000m%}"

# gitのブランチ名を表示するための設定
autoload -Uz vcs_info
setopt prompt_subst
zstyle ':vcs_info:git:*' check-for-changes true
zstyle ':vcs_info:git:*' stagedstr "%F{yellow}!"
zstyle ':vcs_info:git:*' unstagedstr "%F{red}+"
zstyle ':vcs_info:*'     formats "%F{green}%c%u[%b]%f"
zstyle ':vcs_info:*'     actionformats '[%b|%a]'
precmd () { vcs_info }

# コマンドの開始終了時刻を表示するための設定
export PREV_COMMAND_END_TIME
export NEXT_COMMAND_BGN_TIME

function show_command_end_time() {
  PREV_COMMAND_END_TIME=`date "+%H:%M:%S"`
  PROMPT="${COLOR_BGN}${PREV_COMMAND_END_TIME} -          %/${COLOR_END} "'${vcs_info_msg_0_}
'"${COLOR_BGN}\$${COLOR_END}${COLOR_DEFAULT} "
}
autoload -Uz add-zsh-hook
add-zsh-hook precmd show_command_end_time

show_command_begin_time() {
  NEXT_COMMAND_BGN_TIME=`date "+%H:%M:%S"`
  PROMPT="${COLOR_BGN}${PREV_COMMAND_END_TIME} - ${NEXT_COMMAND_BGN_TIME} %/${COLOR_END} "'${vcs_info_msg_0_}
'"${COLOR_BGN}\$${COLOR_END}${COLOR_DEFAULT} "
  zle .accept-line
  zle .reset-prompt
}
zle -N accept-line show_command_begin_time

この設定により、カレントディレクトリのパスと、gitのブランチ名が表示されるようになります。 またプロンプトの背景色が青で着色され、視認性が高くなっています。

f:id:kaitou_ryaku:20191222154857p:plain

これを同僚に見せると「なんかお前の画面、ごちゃごちゃしててキモい...」と言われがちなのですが、しかしこの時刻表示こそ自明に最強であることが明らかなので、全世界が間違ってて私だけが真理を見極めているのだと思います。

予期される反論への反論

「今回の記事の方法でコマンドの実行時間を計測すると、1秒前後の誤差が含まれるじゃないか。俺はミリ秒単位の精度が必要なんだ!!timeコマンドは必要だ!!」 という意見が、競プロ等の界隈から来そうな気がします。 そういうミリ秒レベルの計測値が欲しい場合は、素直にtimeコマンドを叩くのがよろしいと思います。

今回の記事で対象としているのは「実行時間の長いコマンド」の時間計測です。 こうした状況では、有効数字の観点から、ミリ秒単位の精度が要求されることは稀だと思います。 誤差1秒程度なら許されるでしょう。 言い換えると、ミリ秒の精度が要求されるコマンドは、実行時間も短時間(せいぜい数十秒)のはずです。 短時間で終わるので、timeをつけ忘れたときの再実行コストは大したことありません。

そうした短時間のコマンドを実行する場合でも、今回の記事の設定をしておくことで 「コマンドが実行された時刻」 がコンソールバッファに残ります。 いつ何の作業をしたのか分かるのは、それなりに嬉しいことだと思います(historyを適切に設定しろという話ではありますが)。

「長時間計測のtimeつけ忘れを改善したいのなら、素直に zsh-command-time プラグインを使えや」 という反論は、極めて妥当だと思います。 私は全コマンドの実行開始・終了時刻をバッファ内に残したい派閥に属しているので、今回の設定を使っています。 このあたりの価値観は様々で喧嘩しても仕方ないので、お互い尊重して生きていきましょう。

あと冒頭のsleep 10000dを計るには、時刻だけではなく年月日も表示しないといけませんね。 さすがにそこまで表示するのは冗長な気がするので、私は時刻だけ出していますが、このあたりも好き好きといったところでしょうか。


  1. bash3.0以降で、かつHISTTIMEFORMATを設定しておかないと、コマンド開始時刻の検索は不可能なようです。