シェルスクリプト中で、時間を計測したくなったので、それを適用した際の覚書です。

 

 

シェルスクリプトで、サーバーとNAS間でファイルを同期させる、いわゆるバックアップもどきなものを実現させているのですが、その際にバックアップにかかった時間をログに残したくて、その方法を探ってみました。

手順としては、

  1. 作業開始前の時間を取得。
  2. 作業終了後の時間を取得。
  3. 作業後の時刻から作業前の時刻を引いて、差分を求める。

また、

  • 作業開始、終了時刻はタイムスタンプとして習得したい。
  • 経過時間は、無駄にミリ秒単位まで取得したい。

という前提条件で実現させてみます。

 

date コマンド

まず、UNIX系OS で、日付や時刻を取得するためのコマンド、date コマンドについて、簡単に見てみます。

使い方は次の通りです。

date [OPTION]... [+FORMAT]

 

現在時刻を知る

現在のシステムの時刻を表示させるには、単に date のみでOK。

$ date

 

どのような表記になるかは環境によって変わってきますが、概ね次のように表示されます。

ロケールが ja_JP.UTF-8 の場合は次のようになります。

2021年 11月  9日 火曜日 09:05:48 JST

 

ロケールが en_US.UTF-8 の場合は次のようになります。

Tue 09 Nov 2021 08:32:36 AM JST

 

--rfc-3339=FMT オプションを使うと、rfc3339 に準拠したフォーマットで出力できます。

FMT には、datesecondsns が指定できます。

$ date --rfc-3339=seconds
2021-11-09 08:36:53+09:00

 

日付の書式を指定する

+ から始まる日付の書式を指定することで、自由に日付のフォーマットを変えることができます。

$ date +"%Y年%m月%d日"
2021年11月09日

 

主な書式は以下のとおりです。

%d
日付(01..31)
%H
時間(00..23)
%j
年内の日数(001..366)
%m
月(01..12)
%M
分(00..59)
%N
ナノ秒(000000000..999999999)
%s
エポック時間。
1970-01-01 00:00:00 UTC からの秒数。
%S
秒(00..60)
%u
週内の日数(1..7)月曜日は1
%Y
年(2021)

 

日付文字列から

-d オプションを使用することで、日付を表す文字列から日時を表示します。

 

50年前の今日は?

$ date -d "50 years ago"
Tue 09 Nov 1971 09:47:18 AM JST

 

2020年2月の月末は?

$ date -d "2020/2/1 + 1 month - 1 day"
Sat 29 Feb 2020 12:00:00 AM JST

 

ロサンゼルス時間で、来週月曜日の午前10時は、こちらでは何時?

$ date -d 'TZ="America/Los_Angeles" 10:00 next Monday'
Tue 16 Nov 2021 03:00:00 AM JST

 

エポック時間から時刻を表示。

$ date -d '@1636421472'
Tue 09 Nov 2021 10:31:12 AM JST

 

気になるあの人の誕生日は何曜日?

$ date -d '1969/8/31'
Sun Aug 31 00:00:00 JST 1969

 

タイムゾーンを指定する

先程の例で、ある時点のロサンゼルスでの時刻がこちらでは何時か、というコマンドを実行しましたが、単純にロサンゼルスの現在時刻を知りたいときは次のようにします。

$ TZ="America/Los_Angeles" date
Mon 08 Nov 2021 05:21:40 PM PST

タイムゾーンの一覧は、/usr/share/zoneinfo 以下を見るか、

$ timedatectl list-timezones

で知ることができます。

 

ファイルの更新日時を知る

-r FILE オプションを使うことで、指定したファイルの最終更新日時を知ることができます。

$ date -r .bashrc
Tue 25 Feb 2020 09:03:22 PM JST

 

ロケールを変更してみる

ここまでは、使用している Ubuntu が en_US な環境なので、日付と時刻の表記が英語表記になっていましたが、ロケールを変更することで、表記を日本語に変更することができます。

$ LANG=ja_JP.UTF-8 date
2021年 12月 25日 土曜日 14:12:52 JST

 

これは、Ubuntu の環境に限った話ですが、仮にロケールを指定しても日本語で出力されない場合は、言語パックがインストールされていないかもしれません。

$ LC_ALL=ja_JP.UTF-8 date
Sat Dec 25 14:08:36 JST 2021 # 日本表記に切り替わらない

 

locale コマンドでロケールに関する情報を表示させることができます。

ちなみに私の環境(Ubuntu 20.04 LTS)では以下のような表記になりました。

$ locale
LANG=C.UTF-8
LANGUAGE=
LC_CTYPE="C.UTF-8"
LC_NUMERIC="C.UTF-8"
LC_TIME="C.UTF-8"
LC_COLLATE="C.UTF-8"
LC_MONETARY="C.UTF-8"
LC_MESSAGES="C.UTF-8"
LC_PAPER="C.UTF-8"
LC_NAME="C.UTF-8"
LC_ADDRESS="C.UTF-8"
LC_TELEPHONE="C.UTF-8"
LC_MEASUREMENT="C.UTF-8"
LC_IDENTIFICATION="C.UTF-8"
LC_ALL=

 

現在インストールされているロケールを表示させるには、locale -a とします。

$ locale -a
C
C.UTF-8
POSIX
en_US.utf8

 

このように、日本語のロケール(ja_JP.UTF8)がインストールされていないことがわかります。

その場合は、以下のようにして日本語ロケールをインストールします。

$ sudo apt install language-pack-ja

 

date コマンドで経過時間を求める

シェルスクリプト中で、処理にかかった時間を求めたい場合があります。

そんなときどうすればよいのか調べてみました。

 

date コマンドでミリ秒単位で時間を計測する例

まず回りくどい説明は一旦省いて、以下がその例になります。

#!/bin/bash
# 現在時刻を取得
_started_at=$(date +'%s.%3N')

# 何らかの処理
sleep 3

# 完了時刻を取得
_ended_at=$(date +'%s.%3N')

# 経過時間を計算
_elapsed=$(echo "scale=3; $_ended_at - $_started_at" | bc)

echo "start: $(date -d "@${_started_at}" +'%Y-%m-%d %H:%M:%S.%3N (%:z)')"
echo "end  : $(date -d "@${_ended_at}" +'%Y-%m-%d %H:%M:%S.%3N (%:z)')"
echo "dur:   $_elapsed"
eval "echo Elapsed Time: $(date -ud "@$_elapsed" +'$((%s/3600/24)):%H:%M:%S.%3N')"

 

ちなみにこのスクリプトを実行した結果は以下の通りとなります。

start: 2022-01-12 21:34:36.266 (+09:00)
end  : 2022-01-12 21:34:39.273 (+09:00)
dur:   3.007
Elapsed Time: 0:00:00:03.007

 

以降、この例の説明をします。

 

date コマンドでミリ秒単位で時間を計測する方法

まず以下の例ですが、エポックタイムとナノ秒単位で現在時刻を出力させてみた例です。

date +'%s.%N'
1641896590.718841034

 

この例では、書式 %N を使うことで、ナノ秒単位で表示させています。ナノ秒とは、0.000000001 秒のことですね。

ミリ秒の場合は、書式を 0.001 秒単位まで切り詰めないといけません。

そんなときは、書式文字の %N の間に、3 と入れることで、有効桁数を指定することができます。

つまり、以下のようにします。

$ date +"%s.%3N"
1641896999.468

 

書式をタイムスタンプ風に表示するには、次のようにします。

$ date +'%H:%M:%S.%3N'
09:28:09.689

 

これを用いて、スクリプトの処理の開始前と終了後、それぞれで次のようにしてエポックタイムとミリ秒単位の数値を取得します。

# 処理の開始
_started_at=$(date +'%s.%3N')
# 何らかの処理

# 処理の終了
_ended_at=$(date +'%s.%3N')

 

そして、その経過時間を算出し、変数に代入します。

# 経過時間を計算
_elapsed=$(echo "scale=3; $_ended_at - $_started_at" | bc)

 

以上で、経過時間を取得することができました。

 

eval$((expression)) の意味

普通個人レベルで経過時間を測るときに、数日単位で時間がかかるようなことはないと思いますが、念のためにに経過時間の日数を計算できるようにしておきます。

そして、ここがなんともややこしい部分です。

肝心の式の部分を見てみます。

eval "echo Elapsed Time: $(date -ud "@$_elapsed" +'$((%s/3600/24)):%H:%M:%S.%3N')"

 

eval は、Bash の組み込みコマンドになります。以下に、bash のマニュアルを引用します。

eval [arg ...]
引数が読み込まれて、一つのコマンドに連結されます。このコマンドはシェルに読み込まれて実行され、その終了ステータスは eval の値として返されます。一つも引数を指定しないか、引数が null のみの場合、eval は 0 を返します。

 

上の例の場合、まず、内側の $(date -ud ...) が、コマンド置換によって中身が置き換えられ、最終的に eval によってシェルにコマンドとして渡されます。

date の書式指定の中に $((%s/3500/24)) とある部分は bash の算術演算($((expression)))になっていますが、この部分はシェルによって演算されることはなく、%s の部分が -d で指定された文字列のエポックタイムで置き換えられます。

# 置換前($_elapsed=5586991.600 として)
eval "echo Elapsed Time: $(date -ud "@$_elapsed" +'$((%s/3600/24)):%H:%M:%S.%3N')"
        ↓↓↓
echo Elapsed Time: $((5586991/3600/24)):15:56:31.600

 

そして最終的に eval で評価された結果としてものコマンド(echo)で、上の算術演算の式($((5586991/3600/24)))がシェルによって展開されて、日数として返されます。

64:15:56:31.600 # echo Elapsed Time: $((5586991/3600/24)):15:56:31.600 の出力

 

ちなみに、bash の算術演算の展開についても、以下にマニュアルからの引用を提示しておきます。

算術演算の展開
算術演算の展開は、演算式の評価と結果の代入を可能にします。展開の書式は次のとおりです。

$((expression))

古い書式、$[expression] は非推奨で bash 将来のバージョンでは削除される予定です。

式は二重引用符に囲まれているものとして扱われますが、丸括弧内の二重引用符は特別に扱われません。式に含まれる全ての字句はパラメータと変数に展開され、コマンドの置換、引用符の削除が行われます。結果は評価される算術式として取り扱われます。算術式は入れ子にすることができます。

式の評価は下に記載されている 算術評価 のルールにしたがって実行されます。もし式が無効な場合は、bash は失敗を示すメッセージを表示し、代入は行われません。

 

まとめ

以上、時間をミリ秒単位で計測したい、という部分から date コマンドや、bash の組み込みコマンド等様々な部分に言及が広がりましたが、やはりマニュアルを読む、ということは結構基本になることがよくわかりました。


 

 

zaturendo

中小企業社内SE。

0件のコメント

コメントを残す

アバタープレースホルダー

メールアドレスが公開されることはありません。 が付いている欄は必須項目です